From 82a8f3351f935a9c6c61102b598a57899cfca5c1 Mon Sep 17 00:00:00 2001
From: xuexihuang <1339326187@qq.com>
Date: Tue, 7 Nov 2023 14:36:56 +0800
Subject: [PATCH 01/15] Add Prometheus monitoring function (#1337)
* Code adaptation k8s: service discovery and registration adaptation, configuration adaptation
* Initial submission of the help charts script for openim API
* change the help charts script
* change the help charts script
* change helm chart codes
* change dockerfiles script
* change chart script:add configmap mounts
* change chart script:change repository
* change chart script:msggateway add one service
* change config.yaml
* roll back some config values
* change chart script:change Ingress rule with a rewrite annotation
* add mysql charts scrible
* change chart script:add mysql.config.yaml
* add nfs provisioner charts
* change chart script:add nfs.config.yaml
* add ingress-nginx charts
* change chart script:add ingress-nginx.config.yaml
* add redis &mongodb charts
* add kafka&minio charts
* change chart script:change redis.values.yaml
* change chart script:add redis.config.yaml
* change chart script:change redis.config.yaml
* change chart script:change mongodb.value.yaml
* change chart script:change mongodb.value.yaml
* change chart script:add mongodb.config.yaml
* change chart script:change minio.values.yaml
* change chart script:add minio.config.yaml
* change chart script:change kafka.values.yaml
* change chart script:add kafka.config.yaml
* change chart script:change services.config.yaml
* bug fix:Delete websocket's Port restrictions
* bug fix:change port value
* change chart script:Submit a stable version script
* fix bug:Implement option interface
* fix bug:change K8sDR.Register
* change config.yaml
* change chats script:minio service add ingress
* change chats script:minio service add ingress
* change chats script:kafka.replicaCount=3& change minio.api ingress
* delete change chats script
* change config.yaml
* change openim.yaml
* merge go.sum
* Add monitoring function and struct for Prometheus on gin and GRPC
* Add GRPC and gin server monitoring logic
* Add GRPC and gin server monitoring logic2
* Add GRPC and gin server monitoring logic3
* Add GRPC and gin server monitoring logic4
* Add GRPC and gin server monitoring logic5
* Add GRPC and gin server monitoring logic6
* Add GRPC and gin server monitoring logic7
* delete:old monitoring code
* add for test
* fix bug:change packname
* fix bug:delete getPromPort funciton
* fix bug:delete getPromPort funciton
* fix bug:change logs
* fix bug:change registerName logic in GetGrpcCusMetrics function
* add getPrometheus url api
* fix:config path logic
* fix:prometheus enable function
* fix:prometheus enable function
* fix:transfer Multi process monitoring logic
* del:del not using manifest
* fix:openim-msgtransfer.sh
* fix:openim-msgtransfer.sh
---------
Co-authored-by: lin.huang
Co-authored-by: Xinwei Xiong <3293172751@qq.com>
---
cmd/openim-api/main.go | 17 +-
cmd/openim-msgtransfer/main.go | 1 +
config/config.yaml | 4 +-
deployments/templates/openim.yaml | 2 +
go.mod | 2 +-
go.sum | 4 +-
internal/api/route.go | 9 +-
internal/api/third.go | 5 +
internal/msggateway/hub_server.go | 5 +-
internal/msggateway/init.go | 2 +-
internal/msggateway/n_ws_server.go | 3 +
internal/msgtransfer/init.go | 42 +-
.../online_msg_to_mongo_handler.go | 4 +
internal/push/consumer_init.go | 9 -
internal/push/push_rpc_server.go | 1 -
internal/push/push_to_client.go | 5 +-
internal/rpc/auth/auth.go | 2 +
internal/rpc/msg/send.go | 17 +-
internal/rpc/msg/server.go | 18 -
pkg/common/cmd/api.go | 10 +-
pkg/common/cmd/msg_gateway.go | 2 +-
pkg/common/cmd/msg_transfer.go | 25 +
pkg/common/cmd/rpc.go | 24 +
pkg/common/config/config.go | 26 +-
pkg/common/config/parse.go | 14 +-
pkg/common/db/controller/msg.go | 36 +-
pkg/common/ginPrometheus/ginPrometheus.go | 417 ++++++++++++++++
pkg/common/kafka/producer.go | 6 +-
pkg/common/prom_metrics/func.go | 45 ++
pkg/common/prom_metrics/gin-api.go | 16 +
pkg/common/prom_metrics/grpc-auth.go | 12 +
pkg/common/prom_metrics/grpc-msg.go | 24 +
pkg/common/prom_metrics/grpc-msggateway.go | 12 +
pkg/common/prom_metrics/grpc_push.go | 12 +
pkg/common/prom_metrics/transfer.go | 28 ++
pkg/common/prome/doc.go | 15 -
pkg/common/prome/gather.go | 470 ------------------
pkg/common/prome/prometheus.go | 97 ----
pkg/common/startrpc/start.go | 30 +-
scripts/install/environment.sh | 3 +
scripts/install/openim-msgtransfer.sh | 4 +-
41 files changed, 760 insertions(+), 720 deletions(-)
create mode 100644 pkg/common/ginPrometheus/ginPrometheus.go
create mode 100644 pkg/common/prom_metrics/func.go
create mode 100644 pkg/common/prom_metrics/gin-api.go
create mode 100644 pkg/common/prom_metrics/grpc-auth.go
create mode 100644 pkg/common/prom_metrics/grpc-msg.go
create mode 100644 pkg/common/prom_metrics/grpc-msggateway.go
create mode 100644 pkg/common/prom_metrics/grpc_push.go
create mode 100644 pkg/common/prom_metrics/transfer.go
delete mode 100644 pkg/common/prome/doc.go
delete mode 100644 pkg/common/prome/gather.go
delete mode 100644 pkg/common/prome/prometheus.go
diff --git a/cmd/openim-api/main.go b/cmd/openim-api/main.go
index 174300dc7..0f50c621f 100644
--- a/cmd/openim-api/main.go
+++ b/cmd/openim-api/main.go
@@ -17,6 +17,8 @@ package main
import (
"context"
"fmt"
+ ginProm "github.com/openimsdk/open-im-server/v3/pkg/common/ginPrometheus"
+ "github.com/openimsdk/open-im-server/v3/pkg/common/prom_metrics"
"net"
_ "net/http/pprof"
"strconv"
@@ -43,11 +45,11 @@ func main() {
}
}
-func run(port int) error {
- log.ZInfo(context.Background(), "Openim api port:", "port", port)
+func run(port int, proPort int) error {
+ log.ZInfo(context.Background(), "Openim api port:", "port", port, "proPort", proPort)
- if port == 0 {
- err := "port is empty"
+ if port == 0 || proPort == 0 {
+ err := "port or proPort is empty:" + strconv.Itoa(port) + "," + strconv.Itoa(proPort)
log.ZError(context.Background(), err, nil)
return fmt.Errorf(err)
@@ -82,6 +84,13 @@ func run(port int) error {
}
log.ZInfo(context.Background(), "api register public config to discov success")
router := api.NewGinRouter(client, rdb)
+ //////////////////////////////
+ if config.Config.Prometheus.Enable {
+ p := ginProm.NewPrometheus("app", prom_metrics.GetGinCusMetrics("Api"))
+ p.SetListenAddress(fmt.Sprintf(":%d", proPort))
+ p.Use(router)
+ }
+ /////////////////////////////////
log.ZInfo(context.Background(), "api init router success")
var address string
if config.Config.Api.ListenIP != "" {
diff --git a/cmd/openim-msgtransfer/main.go b/cmd/openim-msgtransfer/main.go
index 722bf5960..6895bcecc 100644
--- a/cmd/openim-msgtransfer/main.go
+++ b/cmd/openim-msgtransfer/main.go
@@ -21,6 +21,7 @@ import (
func main() {
msgTransferCmd := cmd.NewMsgTransferCmd()
msgTransferCmd.AddPrometheusPortFlag()
+ msgTransferCmd.AddTransferProgressFlag()
if err := msgTransferCmd.Exec(); err != nil {
panic(err.Error())
}
diff --git a/config/config.yaml b/config/config.yaml
index 81da293cb..e375118f7 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -382,7 +382,9 @@ callback:
# The number of Prometheus ports per service needs to correspond to rpcPort
# The number of ports needs to be consistent with msg_transfer_service_num in script/path_info.sh
prometheus:
- enable: false
+ enable: true
+ prometheusUrl: "https://openim.prometheus"
+ apiPrometheusPort: [20100]
userPrometheusPort: [ 20110 ]
friendPrometheusPort: [ 20120 ]
messagePrometheusPort: [ 20130 ]
diff --git a/deployments/templates/openim.yaml b/deployments/templates/openim.yaml
index 92eb1cd45..44a28adff 100644
--- a/deployments/templates/openim.yaml
+++ b/deployments/templates/openim.yaml
@@ -383,6 +383,8 @@ callback:
# The number of ports needs to be consistent with msg_transfer_service_num in script/path_info.sh
prometheus:
enable: ${PROMETHEUS_ENABLE}
+ prometheusUrl: ${PROMETHEUS_URL}
+ apiPrometheusPort: [${API_PROM_PORT}]
userPrometheusPort: [ ${USER_PROM_PORT} ]
friendPrometheusPort: [ ${FRIEND_PROM_PORT} ]
messagePrometheusPort: [ ${MESSAGE_PROM_PORT} ]
diff --git a/go.mod b/go.mod
index 64a4db405..31f067693 100644
--- a/go.mod
+++ b/go.mod
@@ -37,7 +37,7 @@ require github.com/google/uuid v1.3.1
require (
github.com/IBM/sarama v1.41.3
- github.com/OpenIMSDK/protocol v0.0.30
+ github.com/OpenIMSDK/protocol v0.0.31
github.com/OpenIMSDK/tools v0.0.16
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible
github.com/go-redis/redis v6.15.9+incompatible
diff --git a/go.sum b/go.sum
index 4116d5428..2fa9b872c 100644
--- a/go.sum
+++ b/go.sum
@@ -18,8 +18,8 @@ firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIw
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/IBM/sarama v1.41.3 h1:MWBEJ12vHC8coMjdEXFq/6ftO6DUZnQlFYcxtOJFa7c=
github.com/IBM/sarama v1.41.3/go.mod h1:Xxho9HkHd4K/MDUo/T/sOqwtX/17D33++E9Wib6hUdQ=
-github.com/OpenIMSDK/protocol v0.0.30 h1:MiHO6PyQMR9ojBHNnSFxCHLmsoE2xZqaiYj975JiZnM=
-github.com/OpenIMSDK/protocol v0.0.30/go.mod h1:F25dFrwrIx3lkNoiuf6FkCfxuwf8L4Z8UIsdTHP/r0Y=
+github.com/OpenIMSDK/protocol v0.0.31 h1:ax43x9aqA6EKNXNukS5MT5BSTqkUmwO4uTvbJLtzCgE=
+github.com/OpenIMSDK/protocol v0.0.31/go.mod h1:F25dFrwrIx3lkNoiuf6FkCfxuwf8L4Z8UIsdTHP/r0Y=
github.com/OpenIMSDK/tools v0.0.16 h1:te/GIq2imCMsrRPgU9OObYKbzZ3rT08Lih/o+3QFIz0=
github.com/OpenIMSDK/tools v0.0.16/go.mod h1:eg+q4A34Qmu73xkY0mt37FHGMCMfC6CtmOnm0kFEGFI=
github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM=
diff --git a/internal/api/route.go b/internal/api/route.go
index d714270b4..7a331d643 100644
--- a/internal/api/route.go
+++ b/internal/api/route.go
@@ -39,7 +39,6 @@ import (
"github.com/OpenIMSDK/tools/mw"
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
- "github.com/openimsdk/open-im-server/v3/pkg/common/prome"
"github.com/openimsdk/open-im-server/v3/pkg/rpcclient"
)
@@ -63,13 +62,6 @@ func NewGinRouter(discov discoveryregistry.SvcDiscoveryRegistry, rdb redis.Unive
u := NewUserApi(*userRpc)
m := NewMessageApi(messageRpc, userRpc)
- if config.Config.Prometheus.Enable {
- prome.NewApiRequestCounter()
- prome.NewApiRequestFailedCounter()
- prome.NewApiRequestSuccessCounter()
- r.Use(prome.PrometheusMiddleware)
- r.GET("/metrics", prome.PrometheusHandler())
- }
ParseToken := GinParseToken(rdb)
userRouterGroup := r.Group("/user")
{
@@ -151,6 +143,7 @@ func NewGinRouter(discov discoveryregistry.SvcDiscoveryRegistry, rdb redis.Unive
// Third service
thirdGroup := r.Group("/third", ParseToken)
{
+ thirdGroup.GET("/prometheus", GetPrometheus)
t := NewThirdApi(*thirdRpc)
thirdGroup.POST("/fcm_update_token", t.FcmUpdateToken)
thirdGroup.POST("/set_app_badge", t.SetAppBadge)
diff --git a/internal/api/third.go b/internal/api/third.go
index cdb059cc0..fca133ea9 100644
--- a/internal/api/third.go
+++ b/internal/api/third.go
@@ -15,6 +15,7 @@
package api
import (
+ config2 "github.com/openimsdk/open-im-server/v3/pkg/common/config"
"math/rand"
"net/http"
"strconv"
@@ -118,3 +119,7 @@ func (o *ThirdApi) DeleteLogs(c *gin.Context) {
func (o *ThirdApi) SearchLogs(c *gin.Context) {
a2r.Call(third.ThirdClient.SearchLogs, o.Client, c)
}
+
+func GetPrometheus(c *gin.Context) {
+ c.Redirect(http.StatusFound, config2.Config.Prometheus.PrometheusUrl)
+}
diff --git a/internal/msggateway/hub_server.go b/internal/msggateway/hub_server.go
index ae12c04a3..670757850 100644
--- a/internal/msggateway/hub_server.go
+++ b/internal/msggateway/hub_server.go
@@ -33,7 +33,6 @@ import (
"github.com/OpenIMSDK/tools/utils"
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
- "github.com/openimsdk/open-im-server/v3/pkg/common/prome"
"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc"
)
@@ -69,9 +68,10 @@ func (s *Server) SetLongConnServer(LongConnServer LongConnServer) {
s.LongConnServer = LongConnServer
}
-func NewServer(rpcPort int, longConnServer LongConnServer) *Server {
+func NewServer(rpcPort int, proPort int, longConnServer LongConnServer) *Server {
return &Server{
rpcPort: rpcPort,
+ prometheusPort: proPort,
LongConnServer: longConnServer,
pushTerminal: []int{constant.IOSPlatformID, constant.AndroidPlatformID},
}
@@ -158,7 +158,6 @@ func (s *Server) SuperGroupOnlineBatchPushOneMsg(
} else {
if utils.IsContainInt(client.PlatformID, s.pushTerminal) {
tempT.OnlinePush = true
- prome.Inc(prome.MsgOnlinePushSuccessCounter)
resp = append(resp, temp)
}
}
diff --git a/internal/msggateway/init.go b/internal/msggateway/init.go
index ce63fb21a..94f1b2011 100644
--- a/internal/msggateway/init.go
+++ b/internal/msggateway/init.go
@@ -41,7 +41,7 @@ func RunWsAndServer(rpcPort, wsPort, prometheusPort int) error {
if err != nil {
return err
}
- hubServer := NewServer(rpcPort, longServer)
+ hubServer := NewServer(rpcPort, prometheusPort, longServer)
go func() {
err := hubServer.Start()
if err != nil {
diff --git a/internal/msggateway/n_ws_server.go b/internal/msggateway/n_ws_server.go
index 83e297502..ad56c1373 100644
--- a/internal/msggateway/n_ws_server.go
+++ b/internal/msggateway/n_ws_server.go
@@ -17,6 +17,7 @@ package msggateway
import (
"context"
"errors"
+ "github.com/openimsdk/open-im-server/v3/pkg/common/prom_metrics"
"net/http"
"strconv"
"sync"
@@ -220,6 +221,7 @@ func (ws *WsServer) registerClient(client *Client) {
if !userOK {
ws.clients.Set(client.UserID, client)
log.ZDebug(client.ctx, "user not exist", "userID", client.UserID, "platformID", client.PlatformID)
+ prom_metrics.OnlineUserGauge.Add(1)
ws.onlineUserNum.Add(1)
ws.onlineUserConnNum.Add(1)
} else {
@@ -364,6 +366,7 @@ func (ws *WsServer) unregisterClient(client *Client) {
isDeleteUser := ws.clients.delete(client.UserID, client.ctx.GetRemoteAddr())
if isDeleteUser {
ws.onlineUserNum.Add(-1)
+ prom_metrics.OnlineUserGauge.Dec()
}
ws.onlineUserConnNum.Add(-1)
ws.SetUserOnlineStatus(client.ctx, client, constant.Offline)
diff --git a/internal/msgtransfer/init.go b/internal/msgtransfer/init.go
index 7efc35794..c18186fa8 100644
--- a/internal/msgtransfer/init.go
+++ b/internal/msgtransfer/init.go
@@ -15,13 +15,18 @@
package msgtransfer
import (
+ "errors"
"fmt"
- "sync"
-
+ "github.com/openimsdk/open-im-server/v3/pkg/common/discovery_register"
+ "github.com/openimsdk/open-im-server/v3/pkg/common/prom_metrics"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/collectors"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
-
- "github.com/openimsdk/open-im-server/v3/pkg/common/discovery_register"
+ "log"
+ "net/http"
+ "sync"
"github.com/OpenIMSDK/tools/mw"
@@ -31,7 +36,6 @@ import (
"github.com/openimsdk/open-im-server/v3/pkg/common/db/relation"
relationtb "github.com/openimsdk/open-im-server/v3/pkg/common/db/table/relation"
"github.com/openimsdk/open-im-server/v3/pkg/common/db/unrelation"
- "github.com/openimsdk/open-im-server/v3/pkg/common/prome"
"github.com/openimsdk/open-im-server/v3/pkg/rpcclient"
)
@@ -81,7 +85,6 @@ func StartTransfer(prometheusPort int) error {
conversationRpcClient := rpcclient.NewConversationRpcClient(client)
groupRpcClient := rpcclient.NewGroupRpcClient(client)
msgTransfer := NewMsgTransfer(chatLogDatabase, msgDatabase, &conversationRpcClient, &groupRpcClient)
- msgTransfer.initPrometheus()
return msgTransfer.Start(prometheusPort)
}
@@ -95,21 +98,13 @@ func NewMsgTransfer(chatLogDatabase controller.ChatLogDatabase,
}
}
-func (m *MsgTransfer) initPrometheus() {
- prome.NewSeqGetSuccessCounter()
- prome.NewSeqGetFailedCounter()
- prome.NewSeqSetSuccessCounter()
- prome.NewSeqSetFailedCounter()
- prome.NewMsgInsertRedisSuccessCounter()
- prome.NewMsgInsertRedisFailedCounter()
- prome.NewMsgInsertMongoSuccessCounter()
- prome.NewMsgInsertMongoFailedCounter()
-}
-
func (m *MsgTransfer) Start(prometheusPort int) error {
var wg sync.WaitGroup
wg.Add(1)
fmt.Println("start msg transfer", "prometheusPort:", prometheusPort)
+ if prometheusPort <= 0 {
+ return errors.New("prometheusPort not correct")
+ }
if config.Config.ChatPersistenceMysql {
// go m.persistentCH.persistentConsumerGroup.RegisterHandleAndConsumer(m.persistentCH)
} else {
@@ -118,10 +113,21 @@ func (m *MsgTransfer) Start(prometheusPort int) error {
go m.historyCH.historyConsumerGroup.RegisterHandleAndConsumer(m.historyCH)
go m.historyMongoCH.historyConsumerGroup.RegisterHandleAndConsumer(m.historyMongoCH)
// go m.modifyCH.modifyMsgConsumerGroup.RegisterHandleAndConsumer(m.modifyCH)
- err := prome.StartPrometheusSrv(prometheusPort)
+ /*err := prome.StartPrometheusSrv(prometheusPort)
if err != nil {
return err
+ }*/
+ ////////////////////////////
+ if config.Config.Prometheus.Enable {
+ reg := prometheus.NewRegistry()
+ reg.MustRegister(
+ collectors.NewGoCollector(),
+ )
+ reg.MustRegister(prom_metrics.GetGrpcCusMetrics("Transfer")...)
+ http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg}))
+ log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", prometheusPort), nil))
}
+ ////////////////////////////////////////
wg.Wait()
return nil
}
diff --git a/internal/msgtransfer/online_msg_to_mongo_handler.go b/internal/msgtransfer/online_msg_to_mongo_handler.go
index bfea6c433..88fd256d1 100644
--- a/internal/msgtransfer/online_msg_to_mongo_handler.go
+++ b/internal/msgtransfer/online_msg_to_mongo_handler.go
@@ -16,6 +16,7 @@ package msgtransfer
import (
"context"
+ "github.com/openimsdk/open-im-server/v3/pkg/common/prom_metrics"
"github.com/IBM/sarama"
"google.golang.org/protobuf/proto"
@@ -74,6 +75,9 @@ func (mc *OnlineHistoryMongoConsumerHandler) handleChatWs2Mongo(
"conversationID",
msgFromMQ.ConversationID,
)
+ prom_metrics.MsgInsertMongoFailedCounter.Inc()
+ } else {
+ prom_metrics.MsgInsertMongoSuccessCounter.Inc()
}
var seqs []int64
for _, msg := range msgFromMQ.MsgData {
diff --git a/internal/push/consumer_init.go b/internal/push/consumer_init.go
index 8595c1656..b72c32bb1 100644
--- a/internal/push/consumer_init.go
+++ b/internal/push/consumer_init.go
@@ -14,10 +14,6 @@
package push
-import (
- "github.com/openimsdk/open-im-server/v3/pkg/common/prome"
-)
-
type Consumer struct {
pushCh ConsumerHandler
successCount uint64
@@ -29,11 +25,6 @@ func NewConsumer(pusher *Pusher) *Consumer {
}
}
-func (c *Consumer) initPrometheus() {
- prome.NewMsgOfflinePushSuccessCounter()
- prome.NewMsgOfflinePushFailedCounter()
-}
-
func (c *Consumer) Start() {
// statistics.NewStatistics(&c.successCount, config.Config.ModuleName.PushName, fmt.Sprintf("%d second push to
// msg_gateway count", constant.StatisticsTimeInterval), constant.StatisticsTimeInterval)
diff --git a/internal/push/push_rpc_server.go b/internal/push/push_rpc_server.go
index 6e9c56023..0f8f36a49 100644
--- a/internal/push/push_rpc_server.go
+++ b/internal/push/push_rpc_server.go
@@ -67,7 +67,6 @@ func Start(client discoveryregistry.SvcDiscoveryRegistry, server *grpc.Server) e
go func() {
defer wg.Done()
consumer := NewConsumer(pusher)
- consumer.initPrometheus()
consumer.Start()
}()
wg.Wait()
diff --git a/internal/push/push_to_client.go b/internal/push/push_to_client.go
index 61d094d27..2f3156c28 100644
--- a/internal/push/push_to_client.go
+++ b/internal/push/push_to_client.go
@@ -18,6 +18,7 @@ import (
"context"
"encoding/json"
"errors"
+ "github.com/openimsdk/open-im-server/v3/pkg/common/prom_metrics"
"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/dummy"
"github.com/OpenIMSDK/protocol/conversation"
@@ -40,7 +41,6 @@ import (
"github.com/openimsdk/open-im-server/v3/pkg/common/db/cache"
"github.com/openimsdk/open-im-server/v3/pkg/common/db/controller"
"github.com/openimsdk/open-im-server/v3/pkg/common/db/localcache"
- "github.com/openimsdk/open-im-server/v3/pkg/common/prome"
"github.com/openimsdk/open-im-server/v3/pkg/rpcclient"
)
@@ -288,10 +288,9 @@ func (p *Pusher) offlinePushMsg(ctx context.Context, conversationID string, msg
}
err = p.offlinePusher.Push(ctx, offlinePushUserIDs, title, content, opts)
if err != nil {
- prome.Inc(prome.MsgOfflinePushFailedCounter)
+ prom_metrics.MsgOfflinePushFailedCounter.Inc()
return err
}
- prome.Inc(prome.MsgOfflinePushSuccessCounter)
return nil
}
diff --git a/internal/rpc/auth/auth.go b/internal/rpc/auth/auth.go
index 9580ef8db..bcca59152 100644
--- a/internal/rpc/auth/auth.go
+++ b/internal/rpc/auth/auth.go
@@ -16,6 +16,7 @@ package auth
import (
"context"
+ "github.com/openimsdk/open-im-server/v3/pkg/common/prom_metrics"
"github.com/openimsdk/open-im-server/v3/pkg/authverify"
@@ -73,6 +74,7 @@ func (s *authServer) UserToken(ctx context.Context, req *pbauth.UserTokenReq) (*
if err != nil {
return nil, err
}
+ prom_metrics.UserLoginCounter.Inc()
resp.Token = token
resp.ExpireTimeSeconds = config.Config.TokenPolicy.Expire * 24 * 60 * 60
return &resp, nil
diff --git a/internal/rpc/msg/send.go b/internal/rpc/msg/send.go
index 2add9c0d1..b43bc82be 100644
--- a/internal/rpc/msg/send.go
+++ b/internal/rpc/msg/send.go
@@ -16,6 +16,7 @@ package msg
import (
"context"
+ "github.com/openimsdk/open-im-server/v3/pkg/common/prom_metrics"
"github.com/openimsdk/open-im-server/v3/pkg/msgprocessor"
@@ -28,8 +29,6 @@ import (
"github.com/OpenIMSDK/tools/log"
"github.com/OpenIMSDK/tools/mcontext"
"github.com/OpenIMSDK/tools/utils"
-
- promepkg "github.com/openimsdk/open-im-server/v3/pkg/common/prome"
)
func (m *msgServer) SendMsg(ctx context.Context, req *pbmsg.SendMsgReq) (resp *pbmsg.SendMsgResp, error error) {
@@ -59,9 +58,8 @@ func (m *msgServer) sendMsgSuperGroupChat(
ctx context.Context,
req *pbmsg.SendMsgReq,
) (resp *pbmsg.SendMsgResp, err error) {
- promepkg.Inc(promepkg.WorkSuperGroupChatMsgRecvSuccessCounter)
if err = m.messageVerification(ctx, req); err != nil {
- promepkg.Inc(promepkg.WorkSuperGroupChatMsgProcessFailedCounter)
+ prom_metrics.GroupChatMsgProcessFailedCounter.Inc()
return nil, err
}
if err = callbackBeforeSendGroupMsg(ctx, req); err != nil {
@@ -80,7 +78,7 @@ func (m *msgServer) sendMsgSuperGroupChat(
if err = callbackAfterSendGroupMsg(ctx, req); err != nil {
log.ZWarn(ctx, "CallbackAfterSendGroupMsg", err)
}
- promepkg.Inc(promepkg.WorkSuperGroupChatMsgProcessSuccessCounter)
+ prom_metrics.GroupChatMsgProcessSuccessCounter.Inc()
resp = &pbmsg.SendMsgResp{}
resp.SendTime = req.MsgData.SendTime
resp.ServerMsgID = req.MsgData.ServerMsgID
@@ -133,9 +131,7 @@ func (m *msgServer) sendMsgNotification(
ctx context.Context,
req *pbmsg.SendMsgReq,
) (resp *pbmsg.SendMsgResp, err error) {
- promepkg.Inc(promepkg.SingleChatMsgRecvSuccessCounter)
if err := m.MsgDatabase.MsgToMQ(ctx, utils.GenConversationUniqueKeyForSingle(req.MsgData.SendID, req.MsgData.RecvID), req.MsgData); err != nil {
- promepkg.Inc(promepkg.SingleChatMsgProcessFailedCounter)
return nil, err
}
resp = &pbmsg.SendMsgResp{
@@ -147,7 +143,6 @@ func (m *msgServer) sendMsgNotification(
}
func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq) (resp *pbmsg.SendMsgResp, err error) {
- promepkg.Inc(promepkg.SingleChatMsgRecvSuccessCounter)
if err := m.messageVerification(ctx, req); err != nil {
return nil, err
}
@@ -166,7 +161,7 @@ func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq
}
}
if !isSend {
- promepkg.Inc(promepkg.SingleChatMsgProcessFailedCounter)
+ prom_metrics.SingleChatMsgProcessFailedCounter.Inc()
return nil, nil
} else {
if err = callbackBeforeSendSingleMsg(ctx, req); err != nil {
@@ -176,7 +171,7 @@ func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq
return nil, err
}
if err := m.MsgDatabase.MsgToMQ(ctx, utils.GenConversationUniqueKeyForSingle(req.MsgData.SendID, req.MsgData.RecvID), req.MsgData); err != nil {
- promepkg.Inc(promepkg.SingleChatMsgProcessFailedCounter)
+ prom_metrics.SingleChatMsgProcessFailedCounter.Inc()
return nil, err
}
err = callbackAfterSendSingleMsg(ctx, req)
@@ -188,7 +183,7 @@ func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq
ClientMsgID: req.MsgData.ClientMsgID,
SendTime: req.MsgData.SendTime,
}
- promepkg.Inc(promepkg.SingleChatMsgProcessSuccessCounter)
+ prom_metrics.SingleChatMsgProcessSuccessCounter.Inc()
return resp, nil
}
}
diff --git a/internal/rpc/msg/server.go b/internal/rpc/msg/server.go
index e8f80914f..88be287fd 100644
--- a/internal/rpc/msg/server.go
+++ b/internal/rpc/msg/server.go
@@ -28,7 +28,6 @@ import (
"github.com/openimsdk/open-im-server/v3/pkg/common/db/controller"
"github.com/openimsdk/open-im-server/v3/pkg/common/db/localcache"
"github.com/openimsdk/open-im-server/v3/pkg/common/db/unrelation"
- "github.com/openimsdk/open-im-server/v3/pkg/common/prome"
"github.com/openimsdk/open-im-server/v3/pkg/rpcclient"
)
@@ -94,27 +93,10 @@ func Start(client discoveryregistry.SvcDiscoveryRegistry, server *grpc.Server) e
}
s.notificationSender = rpcclient.NewNotificationSender(rpcclient.WithLocalSendMsg(s.SendMsg))
s.addInterceptorHandler(MessageHasReadEnabled)
- s.initPrometheus()
msg.RegisterMsgServer(server, s)
return nil
}
-func (m *msgServer) initPrometheus() {
- prome.NewMsgPullFromRedisSuccessCounter()
- prome.NewMsgPullFromRedisFailedCounter()
- prome.NewMsgPullFromMongoSuccessCounter()
- prome.NewMsgPullFromMongoFailedCounter()
- prome.NewSingleChatMsgRecvSuccessCounter()
- prome.NewGroupChatMsgRecvSuccessCounter()
- prome.NewWorkSuperGroupChatMsgRecvSuccessCounter()
- prome.NewSingleChatMsgProcessSuccessCounter()
- prome.NewSingleChatMsgProcessFailedCounter()
- prome.NewGroupChatMsgProcessSuccessCounter()
- prome.NewGroupChatMsgProcessFailedCounter()
- prome.NewWorkSuperGroupChatMsgProcessSuccessCounter()
- prome.NewWorkSuperGroupChatMsgProcessFailedCounter()
-}
-
func (m *msgServer) conversationAndGetRecvID(conversation *conversation.Conversation, userID string) (recvID string) {
if conversation.ConversationType == constant.SingleChatType ||
conversation.ConversationType == constant.NotificationChatType {
diff --git a/pkg/common/cmd/api.go b/pkg/common/cmd/api.go
index 6c74ddad9..00c6cb241 100644
--- a/pkg/common/cmd/api.go
+++ b/pkg/common/cmd/api.go
@@ -34,9 +34,9 @@ func NewApiCmd() *ApiCmd {
return ret
}
-func (a *ApiCmd) AddApi(f func(port int) error) {
+func (a *ApiCmd) AddApi(f func(port int, promPort int) error) {
a.Command.RunE = func(cmd *cobra.Command, args []string) error {
- return f(a.getPortFlag(cmd))
+ return f(a.getPortFlag(cmd), a.getPrometheusPortFlag(cmd))
}
}
@@ -44,8 +44,8 @@ func (a *ApiCmd) GetPortFromConfig(portType string) int {
fmt.Println("GetPortFromConfig:", portType)
if portType == constant.FlagPort {
return config2.Config.Api.OpenImApiPort[0]
- } else {
-
- return 0
+ } else if portType == constant.FlagPrometheusPort {
+ return config2.Config.Prometheus.ApiPrometheusPort[0]
}
+ return 0
}
diff --git a/pkg/common/cmd/msg_gateway.go b/pkg/common/cmd/msg_gateway.go
index 358b79f9b..7f0abb771 100644
--- a/pkg/common/cmd/msg_gateway.go
+++ b/pkg/common/cmd/msg_gateway.go
@@ -66,7 +66,7 @@ func (m *MsgGatewayCmd) GetPortFromConfig(portType string) int {
} else if portType == constant.FlagPort {
return v3config.Config.LongConnSvr.OpenImMessageGatewayPort[0]
} else if portType == constant.FlagPrometheusPort {
- return 0
+ return v3config.Config.Prometheus.MessageGatewayPrometheusPort[0]
} else {
return 0
}
diff --git a/pkg/common/cmd/msg_transfer.go b/pkg/common/cmd/msg_transfer.go
index 20349ebbb..903d1fb95 100644
--- a/pkg/common/cmd/msg_transfer.go
+++ b/pkg/common/cmd/msg_transfer.go
@@ -15,6 +15,9 @@
package cmd
import (
+ "fmt"
+ "github.com/OpenIMSDK/protocol/constant"
+ config2 "github.com/openimsdk/open-im-server/v3/pkg/common/config"
"github.com/spf13/cobra"
"github.com/openimsdk/open-im-server/v3/internal/msgtransfer"
@@ -40,3 +43,25 @@ func (m *MsgTransferCmd) Exec() error {
m.addRunE()
return m.Execute()
}
+
+func (m *MsgTransferCmd) GetPortFromConfig(portType string) int {
+ fmt.Println("GetPortFromConfig:", portType)
+ if portType == constant.FlagPort {
+ return 0
+ } else if portType == constant.FlagPrometheusPort {
+ n := m.getTransferProgressFlagValue()
+ return config2.Config.Prometheus.MessageTransferPrometheusPort[n]
+ }
+ return 0
+}
+func (m *MsgTransferCmd) AddTransferProgressFlag() {
+ m.Command.Flags().IntP(constant.FlagTransferProgressIndex, "n", 0, "transfer progress index")
+}
+func (m *MsgTransferCmd) getTransferProgressFlagValue() int {
+ nindex, err := m.Command.Flags().GetInt(constant.FlagTransferProgressIndex)
+ if err != nil {
+ fmt.Println("get transfercmd error,make sure it is k8s env or not")
+ return 0
+ }
+ return nindex
+}
diff --git a/pkg/common/cmd/rpc.go b/pkg/common/cmd/rpc.go
index 0ccc37fcb..6266c03b2 100644
--- a/pkg/common/cmd/rpc.go
+++ b/pkg/common/cmd/rpc.go
@@ -61,34 +61,58 @@ func (a *RpcCmd) GetPortFromConfig(portType string) int {
if portType == constant.FlagPort {
return config2.Config.RpcPort.OpenImPushPort[0]
}
+ if portType == constant.FlagPrometheusPort {
+ return config2.Config.Prometheus.PushPrometheusPort[0]
+ }
case RpcAuthServer:
if portType == constant.FlagPort {
return config2.Config.RpcPort.OpenImAuthPort[0]
}
+ if portType == constant.FlagPrometheusPort {
+ return config2.Config.Prometheus.AuthPrometheusPort[0]
+ }
case RpcConversationServer:
if portType == constant.FlagPort {
return config2.Config.RpcPort.OpenImConversationPort[0]
}
+ if portType == constant.FlagPrometheusPort {
+ return config2.Config.Prometheus.ConversationPrometheusPort[0]
+ }
case RpcFriendServer:
if portType == constant.FlagPort {
return config2.Config.RpcPort.OpenImFriendPort[0]
}
+ if portType == constant.FlagPrometheusPort {
+ return config2.Config.Prometheus.FriendPrometheusPort[0]
+ }
case RpcGroupServer:
if portType == constant.FlagPort {
return config2.Config.RpcPort.OpenImGroupPort[0]
}
+ if portType == constant.FlagPrometheusPort {
+ return config2.Config.Prometheus.GroupPrometheusPort[0]
+ }
case RpcMsgServer:
if portType == constant.FlagPort {
return config2.Config.RpcPort.OpenImMessagePort[0]
}
+ if portType == constant.FlagPrometheusPort {
+ return config2.Config.Prometheus.MessagePrometheusPort[0]
+ }
case RpcThirdServer:
if portType == constant.FlagPort {
return config2.Config.RpcPort.OpenImThirdPort[0]
}
+ if portType == constant.FlagPrometheusPort {
+ return config2.Config.Prometheus.ThirdPrometheusPort[0]
+ }
case RpcUserServer:
if portType == constant.FlagPort {
return config2.Config.RpcPort.OpenImUserPort[0]
}
+ if portType == constant.FlagPrometheusPort {
+ return config2.Config.Prometheus.UserPrometheusPort[0]
+ }
}
return 0
}
diff --git a/pkg/common/config/config.go b/pkg/common/config/config.go
index 95f4a864e..d7cecc616 100644
--- a/pkg/common/config/config.go
+++ b/pkg/common/config/config.go
@@ -262,18 +262,20 @@ type configStruct struct {
} `yaml:"callback"`
Prometheus struct {
- Enable bool `yaml:"enable"`
- UserPrometheusPort []int `yaml:"userPrometheusPort"`
- FriendPrometheusPort []int `yaml:"friendPrometheusPort"`
- MessagePrometheusPort []int `yaml:"messagePrometheusPort"`
- MessageGatewayPrometheusPort []int `yaml:"messageGatewayPrometheusPort"`
- GroupPrometheusPort []int `yaml:"groupPrometheusPort"`
- AuthPrometheusPort []int `yaml:"authPrometheusPort"`
- PushPrometheusPort []int `yaml:"pushPrometheusPort"`
- ConversationPrometheusPort []int `yaml:"conversationPrometheusPort"`
- RtcPrometheusPort []int `yaml:"rtcPrometheusPort"`
- MessageTransferPrometheusPort []int `yaml:"messageTransferPrometheusPort"`
- ThirdPrometheusPort []int `yaml:"thirdPrometheusPort"`
+ Enable bool `yaml:"enable"`
+ PrometheusUrl string `yaml:"prometheusUrl"`
+ ApiPrometheusPort []int `yaml:"apiPrometheusPort"`
+ UserPrometheusPort []int `yaml:"userPrometheusPort"`
+ FriendPrometheusPort []int `yaml:"friendPrometheusPort"`
+ MessagePrometheusPort []int `yaml:"messagePrometheusPort"`
+ MessageGatewayPrometheusPort []int `yaml:"messageGatewayPrometheusPort"`
+ GroupPrometheusPort []int `yaml:"groupPrometheusPort"`
+ AuthPrometheusPort []int `yaml:"authPrometheusPort"`
+ PushPrometheusPort []int `yaml:"pushPrometheusPort"`
+ ConversationPrometheusPort []int `yaml:"conversationPrometheusPort"`
+ RtcPrometheusPort []int `yaml:"rtcPrometheusPort"`
+ MessageTransferPrometheusPort []int `yaml:"messageTransferPrometheusPort"`
+ ThirdPrometheusPort []int `yaml:"thirdPrometheusPort"`
} `yaml:"prometheus"`
Notification notification `yaml:"notification"`
}
diff --git a/pkg/common/config/parse.go b/pkg/common/config/parse.go
index fee5efbe9..2a877d69b 100644
--- a/pkg/common/config/parse.go
+++ b/pkg/common/config/parse.go
@@ -35,6 +35,16 @@ const (
DefaultFolderPath = "../config/"
)
+// return absolude path join ../config/, this is k8s container config path
+func GetDefaultConfigPath() string {
+ b, err := filepath.Abs(os.Args[0])
+ if err != nil {
+ fmt.Println("filepath.Abs error,err=", err)
+ return ""
+ }
+ return filepath.Join(filepath.Dir(b), "../config/")
+}
+
// getProjectRoot returns the absolute path of the project root directory
func GetProjectRoot() string {
b, _ := filepath.Abs(os.Args[0])
@@ -65,9 +75,11 @@ func initConfig(config interface{}, configName, configFolderPath string) error {
_, err := os.Stat(configFolderPath)
if err != nil {
if !os.IsNotExist(err) {
+ fmt.Println("stat config path error:", err.Error())
return fmt.Errorf("stat config path error: %w", err)
}
configFolderPath = filepath.Join(GetProjectRoot(), "config", configName)
+ fmt.Println("flag's path,enviment's path,default path all is not exist,using project path:", configFolderPath)
}
data, err := os.ReadFile(configFolderPath)
if err != nil {
@@ -86,7 +98,7 @@ func InitConfig(configFolderPath string) error {
if envConfigPath != "" {
configFolderPath = envConfigPath
} else {
- configFolderPath = DefaultFolderPath
+ configFolderPath = GetDefaultConfigPath()
}
}
diff --git a/pkg/common/db/controller/msg.go b/pkg/common/db/controller/msg.go
index af678f92c..e3b6559dc 100644
--- a/pkg/common/db/controller/msg.go
+++ b/pkg/common/db/controller/msg.go
@@ -17,6 +17,7 @@ package controller
import (
"context"
"errors"
+ "github.com/openimsdk/open-im-server/v3/pkg/common/prom_metrics"
"time"
"github.com/redis/go-redis/v9"
@@ -30,8 +31,6 @@ import (
unrelationtb "github.com/openimsdk/open-im-server/v3/pkg/common/db/table/unrelation"
"github.com/openimsdk/open-im-server/v3/pkg/common/db/unrelation"
"github.com/openimsdk/open-im-server/v3/pkg/common/kafka"
- "github.com/openimsdk/open-im-server/v3/pkg/common/prome"
-
"go.mongodb.org/mongo-driver/mongo"
pbmsg "github.com/OpenIMSDK/protocol/msg"
@@ -355,10 +354,9 @@ func (db *commonMsgDatabase) DelUserDeleteMsgsList(ctx context.Context, conversa
func (db *commonMsgDatabase) BatchInsertChat2Cache(ctx context.Context, conversationID string, msgs []*sdkws.MsgData) (seq int64, isNew bool, err error) {
currentMaxSeq, err := db.cache.GetMaxSeq(ctx, conversationID)
if err != nil && errs.Unwrap(err) != redis.Nil {
- prome.Inc(prome.SeqGetFailedCounter)
+ log.ZError(ctx, "db.cache.GetMaxSeq", err)
return 0, false, err
}
- prome.Inc(prome.SeqGetSuccessCounter)
lenList := len(msgs)
if int64(lenList) > db.msg.GetSingleGocMsgNum() {
return 0, false, errors.New("too large")
@@ -378,23 +376,20 @@ func (db *commonMsgDatabase) BatchInsertChat2Cache(ctx context.Context, conversa
}
failedNum, err := db.cache.SetMessageToCache(ctx, conversationID, msgs)
if err != nil {
- prome.Add(prome.MsgInsertRedisFailedCounter, failedNum)
+ prom_metrics.MsgInsertRedisFailedCounter.Add(float64(failedNum))
log.ZError(ctx, "setMessageToCache error", err, "len", len(msgs), "conversationID", conversationID)
} else {
- prome.Inc(prome.MsgInsertRedisSuccessCounter)
+ prom_metrics.MsgInsertRedisSuccessCounter.Inc()
}
err = db.cache.SetMaxSeq(ctx, conversationID, currentMaxSeq)
if err != nil {
- prome.Inc(prome.SeqSetFailedCounter)
- } else {
- prome.Inc(prome.SeqSetSuccessCounter)
+ log.ZError(ctx, "db.cache.SetMaxSeq error", err, "conversationID", conversationID)
+ prom_metrics.SeqSetFailedCounter.Inc()
}
err2 := db.cache.SetHasReadSeqs(ctx, conversationID, userSeqMap)
if err != nil {
log.ZError(ctx, "SetHasReadSeqs error", err2, "userSeqMap", userSeqMap, "conversationID", conversationID)
- prome.Inc(prome.SeqSetFailedCounter)
- } else {
- prome.Inc(prome.SeqSetSuccessCounter)
+ prom_metrics.SeqSetFailedCounter.Inc()
}
return lastMaxSeq, isNew, utils.Wrap(err, "")
}
@@ -493,7 +488,7 @@ func (db *commonMsgDatabase) GetMsgBySeqsRange(ctx context.Context, userID strin
cachedMsgs, failedSeqs, err := db.cache.GetMessagesBySeq(ctx, conversationID, seqs)
if err != nil {
if err != redis.Nil {
- prome.Add(prome.MsgPullFromRedisFailedCounter, len(failedSeqs))
+
log.ZError(ctx, "get message from redis exception", err, "conversationID", conversationID, "seqs", seqs)
}
}
@@ -530,7 +525,7 @@ func (db *commonMsgDatabase) GetMsgBySeqsRange(ctx context.Context, userID strin
cachedMsgs, failedSeqs2, err := db.cache.GetMessagesBySeq(ctx, conversationID, reGetSeqsCache)
if err != nil {
if err != redis.Nil {
- prome.Add(prome.MsgPullFromRedisFailedCounter, len(failedSeqs2))
+
log.ZError(ctx, "get message from redis exception", err, "conversationID", conversationID, "seqs", reGetSeqsCache)
}
}
@@ -543,14 +538,14 @@ func (db *commonMsgDatabase) GetMsgBySeqsRange(ctx context.Context, userID strin
log.ZDebug(ctx, "msgs not exist in redis", "seqs", failedSeqs)
}
// get from cache or db
- prome.Add(prome.MsgPullFromRedisSuccessCounter, len(successMsgs))
+
if len(failedSeqs) > 0 {
mongoMsgs, err := db.getMsgBySeqsRange(ctx, userID, conversationID, failedSeqs, begin, end)
if err != nil {
- prome.Add(prome.MsgPullFromMongoFailedCounter, len(failedSeqs))
+
return 0, 0, nil, err
}
- prome.Add(prome.MsgPullFromMongoSuccessCounter, len(mongoMsgs))
+
successMsgs = append(successMsgs, mongoMsgs...)
}
@@ -582,7 +577,6 @@ func (db *commonMsgDatabase) GetMsgBySeqs(ctx context.Context, userID string, co
successMsgs, failedSeqs, err := db.cache.GetMessagesBySeq(ctx, conversationID, newSeqs)
if err != nil {
if err != redis.Nil {
- prome.Add(prome.MsgPullFromRedisFailedCounter, len(failedSeqs))
log.ZError(ctx, "get message from redis exception", err, "failedSeqs", failedSeqs, "conversationID", conversationID)
}
}
@@ -602,14 +596,14 @@ func (db *commonMsgDatabase) GetMsgBySeqs(ctx context.Context, userID string, co
"conversationID",
conversationID,
)
- prome.Add(prome.MsgPullFromRedisSuccessCounter, len(successMsgs))
+
if len(failedSeqs) > 0 {
mongoMsgs, err := db.getMsgBySeqs(ctx, userID, conversationID, failedSeqs)
if err != nil {
- prome.Add(prome.MsgPullFromMongoFailedCounter, len(failedSeqs))
+
return 0, 0, nil, err
}
- prome.Add(prome.MsgPullFromMongoSuccessCounter, len(mongoMsgs))
+
successMsgs = append(successMsgs, mongoMsgs...)
}
return minSeq, maxSeq, successMsgs, nil
diff --git a/pkg/common/ginPrometheus/ginPrometheus.go b/pkg/common/ginPrometheus/ginPrometheus.go
new file mode 100644
index 000000000..3f7cd65c4
--- /dev/null
+++ b/pkg/common/ginPrometheus/ginPrometheus.go
@@ -0,0 +1,417 @@
+package ginPrometheus
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "strconv"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+)
+
+var defaultMetricPath = "/metrics"
+
+// counter, counter_vec, gauge, gauge_vec,
+// histogram, histogram_vec, summary, summary_vec
+var reqCnt = &Metric{
+ ID: "reqCnt",
+ Name: "requests_total",
+ Description: "How many HTTP requests processed, partitioned by status code and HTTP method.",
+ Type: "counter_vec",
+ Args: []string{"code", "method", "handler", "host", "url"}}
+
+var reqDur = &Metric{
+ ID: "reqDur",
+ Name: "request_duration_seconds",
+ Description: "The HTTP request latencies in seconds.",
+ Type: "histogram_vec",
+ Args: []string{"code", "method", "url"},
+}
+
+var resSz = &Metric{
+ ID: "resSz",
+ Name: "response_size_bytes",
+ Description: "The HTTP response sizes in bytes.",
+ Type: "summary"}
+
+var reqSz = &Metric{
+ ID: "reqSz",
+ Name: "request_size_bytes",
+ Description: "The HTTP request sizes in bytes.",
+ Type: "summary"}
+
+var standardMetrics = []*Metric{
+ reqCnt,
+ reqDur,
+ resSz,
+ reqSz,
+}
+
+/*
+RequestCounterURLLabelMappingFn is a function which can be supplied to the middleware to control
+the cardinality of the request counter's "url" label, which might be required in some contexts.
+For instance, if for a "/customer/:name" route you don't want to generate a time series for every
+possible customer name, you could use this function:
+
+ func(c *gin.Context) string {
+ url := c.Request.URL.Path
+ for _, p := range c.Params {
+ if p.Key == "name" {
+ url = strings.Replace(url, p.Value, ":name", 1)
+ break
+ }
+ }
+ return url
+ }
+
+which would map "/customer/alice" and "/customer/bob" to their template "/customer/:name".
+*/
+type RequestCounterURLLabelMappingFn func(c *gin.Context) string
+
+// Metric is a definition for the name, description, type, ID, and
+// prometheus.Collector type (i.e. CounterVec, Summary, etc) of each metric
+type Metric struct {
+ MetricCollector prometheus.Collector
+ ID string
+ Name string
+ Description string
+ Type string
+ Args []string
+}
+
+// Prometheus contains the metrics gathered by the instance and its path
+type Prometheus struct {
+ reqCnt *prometheus.CounterVec
+ reqDur *prometheus.HistogramVec
+ reqSz, resSz prometheus.Summary
+ router *gin.Engine
+ listenAddress string
+ Ppg PrometheusPushGateway
+
+ MetricsList []*Metric
+ MetricsPath string
+
+ ReqCntURLLabelMappingFn RequestCounterURLLabelMappingFn
+
+ // gin.Context string to use as a prometheus URL label
+ URLLabelFromContext string
+}
+
+// PrometheusPushGateway contains the configuration for pushing to a Prometheus pushgateway (optional)
+type PrometheusPushGateway struct {
+
+ // Push interval in seconds
+ PushIntervalSeconds time.Duration
+
+ // Push Gateway URL in format http://domain:port
+ // where JOBNAME can be any string of your choice
+ PushGatewayURL string
+
+ // Local metrics URL where metrics are fetched from, this could be ommited in the future
+ // if implemented using prometheus common/expfmt instead
+ MetricsURL string
+
+ // pushgateway job name, defaults to "gin"
+ Job string
+}
+
+// NewPrometheus generates a new set of metrics with a certain subsystem name
+func NewPrometheus(subsystem string, customMetricsList ...[]*Metric) *Prometheus {
+ subsystem = "app"
+
+ var metricsList []*Metric
+
+ if len(customMetricsList) > 1 {
+ panic("Too many args. NewPrometheus( string, ).")
+ } else if len(customMetricsList) == 1 {
+ metricsList = customMetricsList[0]
+ }
+
+ for _, metric := range standardMetrics {
+ metricsList = append(metricsList, metric)
+ }
+
+ p := &Prometheus{
+ MetricsList: metricsList,
+ MetricsPath: defaultMetricPath,
+ ReqCntURLLabelMappingFn: func(c *gin.Context) string {
+ return c.Request.URL.Path
+ },
+ }
+
+ p.registerMetrics(subsystem)
+
+ return p
+}
+
+// SetPushGateway sends metrics to a remote pushgateway exposed on pushGatewayURL
+// every pushIntervalSeconds. Metrics are fetched from metricsURL
+func (p *Prometheus) SetPushGateway(pushGatewayURL, metricsURL string, pushIntervalSeconds time.Duration) {
+ p.Ppg.PushGatewayURL = pushGatewayURL
+ p.Ppg.MetricsURL = metricsURL
+ p.Ppg.PushIntervalSeconds = pushIntervalSeconds
+ p.startPushTicker()
+}
+
+// SetPushGatewayJob job name, defaults to "gin"
+func (p *Prometheus) SetPushGatewayJob(j string) {
+ p.Ppg.Job = j
+}
+
+// SetListenAddress for exposing metrics on address. If not set, it will be exposed at the
+// same address of the gin engine that is being used
+func (p *Prometheus) SetListenAddress(address string) {
+ p.listenAddress = address
+ if p.listenAddress != "" {
+ p.router = gin.Default()
+ }
+}
+
+// SetListenAddressWithRouter for using a separate router to expose metrics. (this keeps things like GET /metrics out of
+// your content's access log).
+func (p *Prometheus) SetListenAddressWithRouter(listenAddress string, r *gin.Engine) {
+ p.listenAddress = listenAddress
+ if len(p.listenAddress) > 0 {
+ p.router = r
+ }
+}
+
+// SetMetricsPath set metrics paths
+func (p *Prometheus) SetMetricsPath(e *gin.Engine) {
+
+ if p.listenAddress != "" {
+ p.router.GET(p.MetricsPath, prometheusHandler())
+ p.runServer()
+ } else {
+ e.GET(p.MetricsPath, prometheusHandler())
+ }
+}
+
+// SetMetricsPathWithAuth set metrics paths with authentication
+func (p *Prometheus) SetMetricsPathWithAuth(e *gin.Engine, accounts gin.Accounts) {
+
+ if p.listenAddress != "" {
+ p.router.GET(p.MetricsPath, gin.BasicAuth(accounts), prometheusHandler())
+ p.runServer()
+ } else {
+ e.GET(p.MetricsPath, gin.BasicAuth(accounts), prometheusHandler())
+ }
+
+}
+
+func (p *Prometheus) runServer() {
+ if p.listenAddress != "" {
+ go p.router.Run(p.listenAddress)
+ }
+}
+
+func (p *Prometheus) getMetrics() []byte {
+ response, _ := http.Get(p.Ppg.MetricsURL)
+
+ defer response.Body.Close()
+ body, _ := ioutil.ReadAll(response.Body)
+
+ return body
+}
+
+func (p *Prometheus) getPushGatewayURL() string {
+ h, _ := os.Hostname()
+ if p.Ppg.Job == "" {
+ p.Ppg.Job = "gin"
+ }
+ return p.Ppg.PushGatewayURL + "/metrics/job/" + p.Ppg.Job + "/instance/" + h
+}
+
+func (p *Prometheus) sendMetricsToPushGateway(metrics []byte) {
+ req, err := http.NewRequest("POST", p.getPushGatewayURL(), bytes.NewBuffer(metrics))
+ client := &http.Client{}
+ if _, err = client.Do(req); err != nil {
+ fmt.Println("Error sending to push gateway error:", err.Error())
+ }
+}
+
+func (p *Prometheus) startPushTicker() {
+ ticker := time.NewTicker(time.Second * p.Ppg.PushIntervalSeconds)
+ go func() {
+ for range ticker.C {
+ p.sendMetricsToPushGateway(p.getMetrics())
+ }
+ }()
+}
+
+// NewMetric associates prometheus.Collector based on Metric.Type
+func NewMetric(m *Metric, subsystem string) prometheus.Collector {
+ var metric prometheus.Collector
+ switch m.Type {
+ case "counter_vec":
+ metric = prometheus.NewCounterVec(
+ prometheus.CounterOpts{
+ Subsystem: subsystem,
+ Name: m.Name,
+ Help: m.Description,
+ },
+ m.Args,
+ )
+ case "counter":
+ metric = prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Subsystem: subsystem,
+ Name: m.Name,
+ Help: m.Description,
+ },
+ )
+ case "gauge_vec":
+ metric = prometheus.NewGaugeVec(
+ prometheus.GaugeOpts{
+ Subsystem: subsystem,
+ Name: m.Name,
+ Help: m.Description,
+ },
+ m.Args,
+ )
+ case "gauge":
+ metric = prometheus.NewGauge(
+ prometheus.GaugeOpts{
+ Subsystem: subsystem,
+ Name: m.Name,
+ Help: m.Description,
+ },
+ )
+ case "histogram_vec":
+ metric = prometheus.NewHistogramVec(
+ prometheus.HistogramOpts{
+ Subsystem: subsystem,
+ Name: m.Name,
+ Help: m.Description,
+ },
+ m.Args,
+ )
+ case "histogram":
+ metric = prometheus.NewHistogram(
+ prometheus.HistogramOpts{
+ Subsystem: subsystem,
+ Name: m.Name,
+ Help: m.Description,
+ },
+ )
+ case "summary_vec":
+ metric = prometheus.NewSummaryVec(
+ prometheus.SummaryOpts{
+ Subsystem: subsystem,
+ Name: m.Name,
+ Help: m.Description,
+ },
+ m.Args,
+ )
+ case "summary":
+ metric = prometheus.NewSummary(
+ prometheus.SummaryOpts{
+ Subsystem: subsystem,
+ Name: m.Name,
+ Help: m.Description,
+ },
+ )
+ }
+ return metric
+}
+
+func (p *Prometheus) registerMetrics(subsystem string) {
+
+ for _, metricDef := range p.MetricsList {
+ metric := NewMetric(metricDef, subsystem)
+ if err := prometheus.Register(metric); err != nil {
+ fmt.Println("could not be registered in Prometheus,metricDef.Name:", metricDef.Name, " error:", err.Error())
+ }
+ switch metricDef {
+ case reqCnt:
+ p.reqCnt = metric.(*prometheus.CounterVec)
+ case reqDur:
+ p.reqDur = metric.(*prometheus.HistogramVec)
+ case resSz:
+ p.resSz = metric.(prometheus.Summary)
+ case reqSz:
+ p.reqSz = metric.(prometheus.Summary)
+ }
+ metricDef.MetricCollector = metric
+ }
+}
+
+// Use adds the middleware to a gin engine.
+func (p *Prometheus) Use(e *gin.Engine) {
+ e.Use(p.HandlerFunc())
+ p.SetMetricsPath(e)
+}
+
+// UseWithAuth adds the middleware to a gin engine with BasicAuth.
+func (p *Prometheus) UseWithAuth(e *gin.Engine, accounts gin.Accounts) {
+ e.Use(p.HandlerFunc())
+ p.SetMetricsPathWithAuth(e, accounts)
+}
+
+// HandlerFunc defines handler function for middleware
+func (p *Prometheus) HandlerFunc() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ if c.Request.URL.Path == p.MetricsPath {
+ c.Next()
+ return
+ }
+
+ start := time.Now()
+ reqSz := computeApproximateRequestSize(c.Request)
+
+ c.Next()
+
+ status := strconv.Itoa(c.Writer.Status())
+ elapsed := float64(time.Since(start)) / float64(time.Second)
+ resSz := float64(c.Writer.Size())
+
+ url := p.ReqCntURLLabelMappingFn(c)
+ if len(p.URLLabelFromContext) > 0 {
+ u, found := c.Get(p.URLLabelFromContext)
+ if !found {
+ u = "unknown"
+ }
+ url = u.(string)
+ }
+ p.reqDur.WithLabelValues(status, c.Request.Method, url).Observe(elapsed)
+ p.reqCnt.WithLabelValues(status, c.Request.Method, c.HandlerName(), c.Request.Host, url).Inc()
+ p.reqSz.Observe(float64(reqSz))
+ p.resSz.Observe(resSz)
+ }
+}
+
+func prometheusHandler() gin.HandlerFunc {
+ h := promhttp.Handler()
+ return func(c *gin.Context) {
+ h.ServeHTTP(c.Writer, c.Request)
+ }
+}
+
+func computeApproximateRequestSize(r *http.Request) int {
+ s := 0
+ if r.URL != nil {
+ s = len(r.URL.Path)
+ }
+
+ s += len(r.Method)
+ s += len(r.Proto)
+ for name, values := range r.Header {
+ s += len(name)
+ for _, value := range values {
+ s += len(value)
+ }
+ }
+ s += len(r.Host)
+
+ // r.Form and r.MultipartForm are assumed to be included in r.URL.
+
+ if r.ContentLength != -1 {
+ s += int(r.ContentLength)
+ }
+ return s
+}
diff --git a/pkg/common/kafka/producer.go b/pkg/common/kafka/producer.go
index 4a52d2bef..1766afa97 100644
--- a/pkg/common/kafka/producer.go
+++ b/pkg/common/kafka/producer.go
@@ -28,8 +28,6 @@ import (
"github.com/IBM/sarama"
"google.golang.org/protobuf/proto"
-
- prome "github.com/openimsdk/open-im-server/v3/pkg/common/prome"
)
const (
@@ -131,8 +129,8 @@ func (p *Producer) SendMessage(ctx context.Context, key string, msg proto.Messag
kMsg.Headers = header
partition, offset, err := p.producer.SendMessage(kMsg)
log.ZDebug(ctx, "ByteEncoder SendMessage end", "key ", kMsg.Key, "key length", kMsg.Value.Length())
- if err == nil {
- prome.Inc(prome.SendMsgCounter)
+ if err != nil {
+ log.ZWarn(ctx, "p.producer.SendMessage error", err)
}
return partition, offset, utils.Wrap(err, "")
}
diff --git a/pkg/common/prom_metrics/func.go b/pkg/common/prom_metrics/func.go
new file mode 100644
index 000000000..e451c441b
--- /dev/null
+++ b/pkg/common/prom_metrics/func.go
@@ -0,0 +1,45 @@
+package prom_metrics
+
+import (
+ grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
+ config2 "github.com/openimsdk/open-im-server/v3/pkg/common/config"
+ "github.com/openimsdk/open-im-server/v3/pkg/common/ginPrometheus"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/collectors"
+)
+
+func NewGrpcPromObj(cusMetrics []prometheus.Collector) (*prometheus.Registry, *grpc_prometheus.ServerMetrics, error) {
+ ////////////////////////////////////////////////////////
+ reg := prometheus.NewRegistry()
+ grpcMetrics := grpc_prometheus.NewServerMetrics()
+ grpcMetrics.EnableHandlingTimeHistogram()
+ cusMetrics = append(cusMetrics, grpcMetrics, collectors.NewGoCollector())
+ reg.MustRegister(cusMetrics...)
+ return reg, grpcMetrics, nil
+}
+
+func GetGrpcCusMetrics(registerName string) []prometheus.Collector {
+ switch registerName {
+ case config2.Config.RpcRegisterName.OpenImMessageGatewayName:
+ return []prometheus.Collector{OnlineUserGauge}
+ case config2.Config.RpcRegisterName.OpenImMsgName:
+ return []prometheus.Collector{SingleChatMsgProcessSuccessCounter, SingleChatMsgProcessFailedCounter, GroupChatMsgProcessSuccessCounter, GroupChatMsgProcessFailedCounter}
+ case "Transfer":
+ return []prometheus.Collector{MsgInsertRedisSuccessCounter, MsgInsertRedisFailedCounter, MsgInsertMongoSuccessCounter, MsgInsertMongoFailedCounter, SeqSetFailedCounter}
+ case config2.Config.RpcRegisterName.OpenImPushName:
+ return []prometheus.Collector{MsgOfflinePushFailedCounter}
+ case config2.Config.RpcRegisterName.OpenImAuthName:
+ return []prometheus.Collector{UserLoginCounter}
+ default:
+ return nil
+ }
+}
+
+func GetGinCusMetrics(name string) []*ginPrometheus.Metric {
+ switch name {
+ case "Api":
+ return []*ginPrometheus.Metric{ApiCustomCnt}
+ default:
+ return []*ginPrometheus.Metric{ApiCustomCnt}
+ }
+}
diff --git a/pkg/common/prom_metrics/gin-api.go b/pkg/common/prom_metrics/gin-api.go
new file mode 100644
index 000000000..7aa3f959e
--- /dev/null
+++ b/pkg/common/prom_metrics/gin-api.go
@@ -0,0 +1,16 @@
+package prom_metrics
+
+import ginProm "github.com/openimsdk/open-im-server/v3/pkg/common/ginPrometheus"
+
+/*
+labels := prometheus.Labels{"label_one": "any", "label_two": "value"}
+ApiCustomCnt.MetricCollector.(*prometheus.CounterVec).With(labels).Inc()
+*/
+var (
+ ApiCustomCnt = &ginProm.Metric{
+ Name: "custom_total",
+ Description: "Custom counter events.",
+ Type: "counter_vec",
+ Args: []string{"label_one", "label_two"},
+ }
+)
diff --git a/pkg/common/prom_metrics/grpc-auth.go b/pkg/common/prom_metrics/grpc-auth.go
new file mode 100644
index 000000000..7ca5f1f49
--- /dev/null
+++ b/pkg/common/prom_metrics/grpc-auth.go
@@ -0,0 +1,12 @@
+package prom_metrics
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+var (
+ UserLoginCounter = prometheus.NewCounter(prometheus.CounterOpts{
+ Name: "user_login_total",
+ Help: "The number of user login",
+ })
+)
diff --git a/pkg/common/prom_metrics/grpc-msg.go b/pkg/common/prom_metrics/grpc-msg.go
new file mode 100644
index 000000000..14cb4d858
--- /dev/null
+++ b/pkg/common/prom_metrics/grpc-msg.go
@@ -0,0 +1,24 @@
+package prom_metrics
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+var (
+ SingleChatMsgProcessSuccessCounter = prometheus.NewCounter(prometheus.CounterOpts{
+ Name: "single_chat_msg_process_success_total",
+ Help: "The number of single chat msg successful processed",
+ })
+ SingleChatMsgProcessFailedCounter = prometheus.NewCounter(prometheus.CounterOpts{
+ Name: "single_chat_msg_process_failed_total",
+ Help: "The number of single chat msg failed processed",
+ })
+ GroupChatMsgProcessSuccessCounter = prometheus.NewCounter(prometheus.CounterOpts{
+ Name: "group_chat_msg_process_success_total",
+ Help: "The number of group chat msg successful processed",
+ })
+ GroupChatMsgProcessFailedCounter = prometheus.NewCounter(prometheus.CounterOpts{
+ Name: "group_chat_msg_process_failed_total",
+ Help: "The number of group chat msg failed processed",
+ })
+)
diff --git a/pkg/common/prom_metrics/grpc-msggateway.go b/pkg/common/prom_metrics/grpc-msggateway.go
new file mode 100644
index 000000000..add72e391
--- /dev/null
+++ b/pkg/common/prom_metrics/grpc-msggateway.go
@@ -0,0 +1,12 @@
+package prom_metrics
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+var (
+ OnlineUserGauge = prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "online_user_num",
+ Help: "The number of online user num",
+ })
+)
diff --git a/pkg/common/prom_metrics/grpc_push.go b/pkg/common/prom_metrics/grpc_push.go
new file mode 100644
index 000000000..c05dd6180
--- /dev/null
+++ b/pkg/common/prom_metrics/grpc_push.go
@@ -0,0 +1,12 @@
+package prom_metrics
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+var (
+ MsgOfflinePushFailedCounter = prometheus.NewCounter(prometheus.CounterOpts{
+ Name: "msg_offline_push_failed_total",
+ Help: "The number of msg failed offline pushed",
+ })
+)
diff --git a/pkg/common/prom_metrics/transfer.go b/pkg/common/prom_metrics/transfer.go
new file mode 100644
index 000000000..d3fec47d9
--- /dev/null
+++ b/pkg/common/prom_metrics/transfer.go
@@ -0,0 +1,28 @@
+package prom_metrics
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+var (
+ MsgInsertRedisSuccessCounter = prometheus.NewCounter(prometheus.CounterOpts{
+ Name: "msg_insert_redis_success_total",
+ Help: "The number of successful insert msg to redis",
+ })
+ MsgInsertRedisFailedCounter = prometheus.NewCounter(prometheus.CounterOpts{
+ Name: "msg_insert_redis_failed_total",
+ Help: "The number of failed insert msg to redis",
+ })
+ MsgInsertMongoSuccessCounter = prometheus.NewCounter(prometheus.CounterOpts{
+ Name: "msg_insert_mongo_success_total",
+ Help: "The number of successful insert msg to mongo",
+ })
+ MsgInsertMongoFailedCounter = prometheus.NewCounter(prometheus.CounterOpts{
+ Name: "msg_insert_mongo_failed_total",
+ Help: "The number of failed insert msg to mongo",
+ })
+ SeqSetFailedCounter = prometheus.NewCounter(prometheus.CounterOpts{
+ Name: "seq_set_failed_total",
+ Help: "The number of failed set seq",
+ })
+)
diff --git a/pkg/common/prome/doc.go b/pkg/common/prome/doc.go
deleted file mode 100644
index 7058c711c..000000000
--- a/pkg/common/prome/doc.go
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright © 2023 OpenIM. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package prome // import "github.com/openimsdk/open-im-server/v3/pkg/common/prome"
diff --git a/pkg/common/prome/gather.go b/pkg/common/prome/gather.go
deleted file mode 100644
index eb4bc6c3b..000000000
--- a/pkg/common/prome/gather.go
+++ /dev/null
@@ -1,470 +0,0 @@
-// Copyright © 2023 OpenIM. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package prome
-
-import (
- "github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/promauto"
-)
-
-var (
- // auth rpc.
- UserLoginCounter prometheus.Counter
- UserRegisterCounter prometheus.Counter
-
- // seg.
- SeqGetSuccessCounter prometheus.Counter
- SeqGetFailedCounter prometheus.Counter
- SeqSetSuccessCounter prometheus.Counter
- SeqSetFailedCounter prometheus.Counter
-
- // msg-db.
- MsgInsertRedisSuccessCounter prometheus.Counter
- MsgInsertRedisFailedCounter prometheus.Counter
- MsgInsertMongoSuccessCounter prometheus.Counter
- MsgInsertMongoFailedCounter prometheus.Counter
- MsgPullFromRedisSuccessCounter prometheus.Counter
- MsgPullFromRedisFailedCounter prometheus.Counter
- MsgPullFromMongoSuccessCounter prometheus.Counter
- MsgPullFromMongoFailedCounter prometheus.Counter
-
- // msg-ws.
- MsgRecvTotalCounter prometheus.Counter
- GetNewestSeqTotalCounter prometheus.Counter
- PullMsgBySeqListTotalCounter prometheus.Counter
-
- SingleChatMsgRecvSuccessCounter prometheus.Counter
- GroupChatMsgRecvSuccessCounter prometheus.Counter
- WorkSuperGroupChatMsgRecvSuccessCounter prometheus.Counter
- OnlineUserGauge prometheus.Gauge
-
- // msg-msg.
- SingleChatMsgProcessSuccessCounter prometheus.Counter
- SingleChatMsgProcessFailedCounter prometheus.Counter
- GroupChatMsgProcessSuccessCounter prometheus.Counter
- GroupChatMsgProcessFailedCounter prometheus.Counter
- WorkSuperGroupChatMsgProcessSuccessCounter prometheus.Counter
- WorkSuperGroupChatMsgProcessFailedCounter prometheus.Counter
-
- // msg-push.
- MsgOnlinePushSuccessCounter prometheus.Counter
- MsgOfflinePushSuccessCounter prometheus.Counter
- MsgOfflinePushFailedCounter prometheus.Counter
- // api.
- ApiRequestCounter prometheus.Counter
- ApiRequestSuccessCounter prometheus.Counter
- ApiRequestFailedCounter prometheus.Counter
-
- // grpc.
- GrpcRequestCounter prometheus.Counter
- GrpcRequestSuccessCounter prometheus.Counter
- GrpcRequestFailedCounter prometheus.Counter
-
- SendMsgCounter prometheus.Counter
-
- // conversation.
- ConversationCreateSuccessCounter prometheus.Counter
- ConversationCreateFailedCounter prometheus.Counter
-)
-
-func NewUserLoginCounter() {
- if UserLoginCounter != nil {
- return
- }
- UserLoginCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "user_login",
- Help: "The number of user login",
- })
-}
-
-func NewUserRegisterCounter() {
- if UserRegisterCounter != nil {
- return
- }
- UserRegisterCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "user_register",
- Help: "The number of user register",
- })
-}
-
-func NewSeqGetSuccessCounter() {
- if SeqGetSuccessCounter != nil {
- return
- }
- SeqGetSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "seq_get_success",
- Help: "The number of successful get seq",
- })
-}
-
-func NewSeqGetFailedCounter() {
- if SeqGetFailedCounter != nil {
- return
- }
- SeqGetFailedCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "seq_get_failed",
- Help: "The number of failed get seq",
- })
-}
-
-func NewSeqSetSuccessCounter() {
- if SeqSetSuccessCounter != nil {
- return
- }
- SeqSetSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "seq_set_success",
- Help: "The number of successful set seq",
- })
-}
-
-func NewSeqSetFailedCounter() {
- if SeqSetFailedCounter != nil {
- return
- }
- SeqSetFailedCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "seq_set_failed",
- Help: "The number of failed set seq",
- })
-}
-
-func NewApiRequestCounter() {
- if ApiRequestCounter != nil {
- return
- }
- ApiRequestCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "api_request",
- Help: "The number of api request",
- })
-}
-
-func NewApiRequestSuccessCounter() {
- if ApiRequestSuccessCounter != nil {
- return
- }
- ApiRequestSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "api_request_success",
- Help: "The number of api request success",
- })
-}
-
-func NewApiRequestFailedCounter() {
- if ApiRequestFailedCounter != nil {
- return
- }
- ApiRequestFailedCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "api_request_failed",
- Help: "The number of api request failed",
- })
-}
-
-func NewGrpcRequestCounter() {
- if GrpcRequestCounter != nil {
- return
- }
- GrpcRequestCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "grpc_request",
- Help: "The number of api request",
- })
-}
-
-func NewGrpcRequestSuccessCounter() {
- if GrpcRequestSuccessCounter != nil {
- return
- }
- GrpcRequestSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "grpc_request_success",
- Help: "The number of grpc request success",
- })
-}
-
-func NewGrpcRequestFailedCounter() {
- if GrpcRequestFailedCounter != nil {
- return
- }
- GrpcRequestFailedCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "grpc_request_failed",
- Help: "The number of grpc request failed",
- })
-}
-
-func NewSendMsgCount() {
- if SendMsgCounter != nil {
- return
- }
- SendMsgCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "send_msg",
- Help: "The number of send msg",
- })
-}
-
-func NewMsgInsertRedisSuccessCounter() {
- if MsgInsertRedisSuccessCounter != nil {
- return
- }
- MsgInsertRedisSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "msg_insert_redis_success",
- Help: "The number of successful insert msg to redis",
- })
-}
-
-func NewMsgInsertRedisFailedCounter() {
- if MsgInsertRedisFailedCounter != nil {
- return
- }
- MsgInsertRedisFailedCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "msg_insert_redis_failed",
- Help: "The number of failed insert msg to redis",
- })
-}
-
-func NewMsgInsertMongoSuccessCounter() {
- if MsgInsertMongoSuccessCounter != nil {
- return
- }
- MsgInsertMongoSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "msg_insert_mongo_success",
- Help: "The number of successful insert msg to mongo",
- })
-}
-
-func NewMsgInsertMongoFailedCounter() {
- if MsgInsertMongoFailedCounter != nil {
- return
- }
- MsgInsertMongoFailedCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "msg_insert_mongo_failed",
- Help: "The number of failed insert msg to mongo",
- })
-}
-
-func NewMsgPullFromRedisSuccessCounter() {
- if MsgPullFromRedisSuccessCounter != nil {
- return
- }
- MsgPullFromRedisSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "msg_pull_from_redis_success",
- Help: "The number of successful pull msg from redis",
- })
-}
-
-func NewMsgPullFromRedisFailedCounter() {
- if MsgPullFromRedisFailedCounter != nil {
- return
- }
- MsgPullFromRedisFailedCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "msg_pull_from_redis_failed",
- Help: "The number of failed pull msg from redis",
- })
-}
-
-func NewMsgPullFromMongoSuccessCounter() {
- if MsgPullFromMongoSuccessCounter != nil {
- return
- }
- MsgPullFromMongoSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "msg_pull_from_mongo_success",
- Help: "The number of successful pull msg from mongo",
- })
-}
-
-func NewMsgPullFromMongoFailedCounter() {
- if MsgPullFromMongoFailedCounter != nil {
- return
- }
- MsgPullFromMongoFailedCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "msg_pull_from_mongo_failed",
- Help: "The number of failed pull msg from mongo",
- })
-}
-
-func NewMsgRecvTotalCounter() {
- if MsgRecvTotalCounter != nil {
- return
- }
- MsgRecvTotalCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "msg_recv_total",
- Help: "The number of msg received",
- })
-}
-
-func NewGetNewestSeqTotalCounter() {
- if GetNewestSeqTotalCounter != nil {
- return
- }
- GetNewestSeqTotalCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "get_newest_seq_total",
- Help: "the number of get newest seq",
- })
-}
-
-func NewPullMsgBySeqListTotalCounter() {
- if PullMsgBySeqListTotalCounter != nil {
- return
- }
- PullMsgBySeqListTotalCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "pull_msg_by_seq_list_total",
- Help: "The number of pull msg by seq list",
- })
-}
-
-func NewSingleChatMsgRecvSuccessCounter() {
- if SingleChatMsgRecvSuccessCounter != nil {
- return
- }
- SingleChatMsgRecvSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "single_chat_msg_recv_success",
- Help: "The number of single chat msg successful received ",
- })
-}
-
-func NewGroupChatMsgRecvSuccessCounter() {
- if GroupChatMsgRecvSuccessCounter != nil {
- return
- }
- GroupChatMsgRecvSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "group_chat_msg_recv_success",
- Help: "The number of group chat msg successful received",
- })
-}
-
-func NewWorkSuperGroupChatMsgRecvSuccessCounter() {
- if WorkSuperGroupChatMsgRecvSuccessCounter != nil {
- return
- }
- WorkSuperGroupChatMsgRecvSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "work_super_group_chat_msg_recv_success",
- Help: "The number of work/super group chat msg successful received",
- })
-}
-
-func NewOnlineUserGauges() {
- if OnlineUserGauge != nil {
- return
- }
- OnlineUserGauge = promauto.NewGauge(prometheus.GaugeOpts{
- Name: "online_user_num",
- Help: "The number of online user num",
- })
-}
-
-func NewSingleChatMsgProcessSuccessCounter() {
- if SingleChatMsgProcessSuccessCounter != nil {
- return
- }
- SingleChatMsgProcessSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "single_chat_msg_process_success",
- Help: "The number of single chat msg successful processed",
- })
-}
-
-func NewSingleChatMsgProcessFailedCounter() {
- if SingleChatMsgProcessFailedCounter != nil {
- return
- }
- SingleChatMsgProcessFailedCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "single_chat_msg_process_failed",
- Help: "The number of single chat msg failed processed",
- })
-}
-
-func NewGroupChatMsgProcessSuccessCounter() {
- if GroupChatMsgProcessSuccessCounter != nil {
- return
- }
- GroupChatMsgProcessSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "group_chat_msg_process_success",
- Help: "The number of group chat msg successful processed",
- })
-}
-
-func NewGroupChatMsgProcessFailedCounter() {
- if GroupChatMsgProcessFailedCounter != nil {
- return
- }
- GroupChatMsgProcessFailedCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "group_chat_msg_process_failed",
- Help: "The number of group chat msg failed processed",
- })
-}
-
-func NewWorkSuperGroupChatMsgProcessSuccessCounter() {
- if WorkSuperGroupChatMsgProcessSuccessCounter != nil {
- return
- }
- WorkSuperGroupChatMsgProcessSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "work_super_group_chat_msg_process_success",
- Help: "The number of work/super group chat msg successful processed",
- })
-}
-
-func NewWorkSuperGroupChatMsgProcessFailedCounter() {
- if WorkSuperGroupChatMsgProcessFailedCounter != nil {
- return
- }
- WorkSuperGroupChatMsgProcessFailedCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "work_super_group_chat_msg_process_failed",
- Help: "The number of work/super group chat msg failed processed",
- })
-}
-
-func NewMsgOnlinePushSuccessCounter() {
- if MsgOnlinePushSuccessCounter != nil {
- return
- }
- MsgOnlinePushSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "msg_online_push_success",
- Help: "The number of msg successful online pushed",
- })
-}
-
-func NewMsgOfflinePushSuccessCounter() {
- if MsgOfflinePushSuccessCounter != nil {
- return
- }
- MsgOfflinePushSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "msg_offline_push_success",
- Help: "The number of msg successful offline pushed",
- })
-}
-
-func NewMsgOfflinePushFailedCounter() {
- if MsgOfflinePushFailedCounter != nil {
- return
- }
- MsgOfflinePushFailedCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "msg_offline_push_failed",
- Help: "The number of msg failed offline pushed",
- })
-}
-
-func NewConversationCreateSuccessCounter() {
- if ConversationCreateSuccessCounter != nil {
- return
- }
- ConversationCreateSuccessCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "conversation_push_success",
- Help: "The number of conversation successful pushed",
- })
-}
-
-func NewConversationCreateFailedCounter() {
- if ConversationCreateFailedCounter != nil {
- return
- }
- ConversationCreateFailedCounter = promauto.NewCounter(prometheus.CounterOpts{
- Name: "conversation_push_failed",
- Help: "The number of conversation failed pushed",
- })
-}
diff --git a/pkg/common/prome/prometheus.go b/pkg/common/prome/prometheus.go
deleted file mode 100644
index 254a6c9ea..000000000
--- a/pkg/common/prome/prometheus.go
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright © 2023 OpenIM. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package prome
-
-import (
- "bytes"
- "net/http"
- "strconv"
-
- "github.com/openimsdk/open-im-server/v3/pkg/common/config"
-
- "github.com/gin-gonic/gin"
- "github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/promhttp"
-)
-
-func StartPrometheusSrv(prometheusPort int) error {
- if config.Config.Prometheus.Enable {
- http.Handle("/metrics", promhttp.Handler())
- err := http.ListenAndServe(":"+strconv.Itoa(prometheusPort), nil)
- return err
- }
- return nil
-}
-
-func PrometheusHandler() gin.HandlerFunc {
- h := promhttp.Handler()
- return func(c *gin.Context) {
- h.ServeHTTP(c.Writer, c.Request)
- }
-}
-
-type responseBodyWriter struct {
- gin.ResponseWriter
- body *bytes.Buffer
-}
-
-func (r responseBodyWriter) Write(b []byte) (int, error) {
- r.body.Write(b)
- return r.ResponseWriter.Write(b)
-}
-
-func PrometheusMiddleware(c *gin.Context) {
- Inc(ApiRequestCounter)
- w := &responseBodyWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer}
- c.Writer = w
- c.Next()
- if c.Writer.Status() == http.StatusOK {
- Inc(ApiRequestSuccessCounter)
- } else {
- Inc(ApiRequestFailedCounter)
- }
-}
-
-func Inc(counter prometheus.Counter) {
- if config.Config.Prometheus.Enable {
- if counter != nil {
- counter.Inc()
- }
- }
-}
-
-func Add(counter prometheus.Counter, add int) {
- if config.Config.Prometheus.Enable {
- if counter != nil {
- counter.Add(float64(add))
- }
- }
-}
-
-func GaugeInc(gauges prometheus.Gauge) {
- if config.Config.Prometheus.Enable {
- if gauges != nil {
- gauges.Inc()
- }
- }
-}
-
-func GaugeDec(gauges prometheus.Gauge) {
- if config.Config.Prometheus.Enable {
- if gauges != nil {
- gauges.Dec()
- }
- }
-}
diff --git a/pkg/common/startrpc/start.go b/pkg/common/startrpc/start.go
index f04ab2508..975d21246 100644
--- a/pkg/common/startrpc/start.go
+++ b/pkg/common/startrpc/start.go
@@ -16,7 +16,12 @@ package startrpc
import (
"fmt"
+ "github.com/openimsdk/open-im-server/v3/pkg/common/prom_metrics"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+ "log"
"net"
+ "net/http"
"strconv"
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
@@ -29,7 +34,6 @@ import (
"github.com/OpenIMSDK/tools/discoveryregistry"
"github.com/OpenIMSDK/tools/mw"
"github.com/OpenIMSDK/tools/network"
- "github.com/OpenIMSDK/tools/prome"
"github.com/OpenIMSDK/tools/utils"
)
@@ -41,7 +45,7 @@ func Start(
rpcFn func(client discoveryregistry.SvcDiscoveryRegistry, server *grpc.Server) error,
options ...grpc.ServerOption,
) error {
- fmt.Printf("start %s server, port: %d, prometheusPort: %d, OpenIM version: %s",
+ fmt.Printf("start %s server, port: %d, prometheusPort: %d, OpenIM version: %s\n",
rpcRegisterName, rpcPort, prometheusPort, config.Version)
listener, err := net.Listen(
"tcp",
@@ -61,16 +65,15 @@ func Start(
if err != nil {
return err
}
+ var reg *prometheus.Registry
+ var metric *grpcprometheus.ServerMetrics
// ctx 中间件
if config.Config.Prometheus.Enable {
- prome.NewGrpcRequestCounter()
- prome.NewGrpcRequestFailedCounter()
- prome.NewGrpcRequestSuccessCounter()
- unaryInterceptor := mw.InterceptChain(grpcprometheus.UnaryServerInterceptor, mw.RpcServerInterceptor)
- options = append(options, []grpc.ServerOption{
- grpc.StreamInterceptor(grpcprometheus.StreamServerInterceptor),
- grpc.UnaryInterceptor(unaryInterceptor),
- }...)
+ //////////////////////////
+ cusMetrics := prom_metrics.GetGrpcCusMetrics(rpcRegisterName)
+ reg, metric, err = prom_metrics.NewGrpcPromObj(cusMetrics)
+ options = append(options, mw.GrpcServer(), grpc.StreamInterceptor(metric.StreamServerInterceptor()),
+ grpc.UnaryInterceptor(metric.UnaryServerInterceptor()))
} else {
options = append(options, mw.GrpcServer())
}
@@ -91,8 +94,11 @@ func Start(
}
go func() {
if config.Config.Prometheus.Enable && prometheusPort != 0 {
- if err := prome.StartPrometheusSrv(prometheusPort); err != nil {
- panic(err.Error())
+ metric.InitializeMetrics(srv)
+ // Create a HTTP server for prometheus.
+ httpServer := &http.Server{Handler: promhttp.HandlerFor(reg, promhttp.HandlerOpts{}), Addr: fmt.Sprintf("0.0.0.0:%d", prometheusPort)}
+ if err := httpServer.ListenAndServe(); err != nil {
+ log.Fatal("Unable to start a http server.")
}
}
}()
diff --git a/scripts/install/environment.sh b/scripts/install/environment.sh
index d1ef03197..09fb94e7d 100755
--- a/scripts/install/environment.sh
+++ b/scripts/install/environment.sh
@@ -341,6 +341,9 @@ def "IOS_PRODUCTION" "false" # IOS生产
###################### Prometheus 配置信息 ######################
def "PROMETHEUS_ENABLE" "false" # 是否启用 Prometheus
+def "PROMETHEUS_URL" "/prometheus"
+# Api 服务的 Prometheus 端口
+readonly API_PROM_PORT=${API_PROM_PORT:-'20100'}
# User 服务的 Prometheus 端口
readonly USER_PROM_PORT=${USER_PROM_PORT:-'20110'}
# Friend 服务的 Prometheus 端口
diff --git a/scripts/install/openim-msgtransfer.sh b/scripts/install/openim-msgtransfer.sh
index b28ca7efa..08a7d3ec7 100755
--- a/scripts/install/openim-msgtransfer.sh
+++ b/scripts/install/openim-msgtransfer.sh
@@ -49,13 +49,13 @@ function openim::msgtransfer::start()
openim::log::error_exit "OPENIM_MSGGATEWAY_NUM must be equal to the number of MSG_TRANSFER_PROM_PORTS"
fi
- for (( i=1; i<=$OPENIM_MSGGATEWAY_NUM; i++ )) do
+ for (( i=0; i<$OPENIM_MSGGATEWAY_NUM; i++ )) do
openim::log::info "prometheus port: ${MSG_TRANSFER_PROM_PORTS[$i]}"
PROMETHEUS_PORT_OPTION=""
if [[ -n "${OPENIM_PROMETHEUS_PORTS[$i]}" ]]; then
PROMETHEUS_PORT_OPTION="--prometheus_port ${OPENIM_PROMETHEUS_PORTS[$i]}"
fi
- nohup ${OPENIM_MSGTRANSFER_BINARY} ${PROMETHEUS_PORT_OPTION} -c ${OPENIM_MSGTRANSFER_CONFIG} >> ${LOG_FILE} 2>&1 &
+ nohup ${OPENIM_MSGTRANSFER_BINARY} ${PROMETHEUS_PORT_OPTION} -c ${OPENIM_MSGTRANSFER_CONFIG} -n ${i}>> ${LOG_FILE} 2>&1 &
done
openim::util::check_process_names "${OPENIM_OUTPUT_HOSTBIN}/${SERVER_NAME}"
From 194a94cb8ef918a6e42e538e91e189e548f146af Mon Sep 17 00:00:00 2001
From: chao <48119764+withchao@users.noreply.github.com>
Date: Wed, 8 Nov 2023 03:29:28 -0600
Subject: [PATCH 02/15] fix: initiateUpload sign list number (#1358)
* optimize scheduled deletion
* optimize scheduled deletion
* optimize scheduled deletion
* optimize scheduled deletion
* minio cache
* fix: conflicts
* feat: minio cache
* feat: cache optimize
* feat: cache optimize
* feat: cache optimize
* feat: cache optimize
* feat: cache optimize
* fix: initiateUpload sign list number
---
pkg/common/db/s3/cont/controller.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/common/db/s3/cont/controller.go b/pkg/common/db/s3/cont/controller.go
index 2d9d7d32d..2db1b800f 100644
--- a/pkg/common/db/s3/cont/controller.go
+++ b/pkg/common/db/s3/cont/controller.go
@@ -144,7 +144,7 @@ func (c *Controller) InitiateUpload(ctx context.Context, hash string, size int64
}
var authSign *s3.AuthSignResult
if maxParts > 0 {
- partNumbers := make([]int, partNumber)
+ partNumbers := make([]int, maxParts)
for i := 0; i < maxParts; i++ {
partNumbers[i] = i + 1
}
From 3ba861fd90dafb1fb7a32a045d8ae8cef8d85b5f Mon Sep 17 00:00:00 2001
From: Gordon <46924906+FGadvancer@users.noreply.github.com>
Date: Wed, 8 Nov 2023 18:01:28 +0800
Subject: [PATCH 03/15] fix: msg pull change and fcm redis flag fix. (#1367)
* fix: to start im or chat, ZooKeeper must be started first.
* fix: msg gateway start output err info
Signed-off-by: Gordon <1432970085@qq.com>
* fix: msg gateway start output err info
Signed-off-by: Gordon <1432970085@qq.com>
* chore: package path changes
Signed-off-by: withchao <993506633@qq.com>
* fix: go mod update
Signed-off-by: Gordon <1432970085@qq.com>
* fix: token update
Signed-off-by: Gordon <1432970085@qq.com>
* chore: package path changes
Signed-off-by: withchao <993506633@qq.com>
* chore: package path changes
Signed-off-by: withchao <993506633@qq.com>
* fix: token update
Signed-off-by: Gordon <1432970085@qq.com>
* fix: token update
Signed-off-by: Gordon <1432970085@qq.com>
* fix: token update
Signed-off-by: Gordon <1432970085@qq.com>
* fix: token update
Signed-off-by: Gordon <1432970085@qq.com>
* fix: token update
Signed-off-by: Gordon <1432970085@qq.com>
* fix: token update
Signed-off-by: Gordon <1432970085@qq.com>
* fix: get all userID
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: msggateway add online status call
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* refactor: log change
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* refactor: log change
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* chore: network mode change
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* cicd: robot automated Change
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* cicd: robot automated Change
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* cicd: robot automated Change
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat: add api of get server time
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* feat: remove go work sum
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* cicd: robot automated Change
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix: pull message add isRead field
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: check msg-transfer script
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: script update
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: start don't kill old process
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* cicd: robot automated Change
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix: check component
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: pull message set isRead only message come from single.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* cicd: robot automated Change
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix: multiple gateway kick user each other.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: multiple gateway kick user each other.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: multiple gateway kick user each other.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: multiple gateway kick user each other.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: multiple gateway kick user each other.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: multiple gateway kick user each other.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* cicd: robot automated Change
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix: multiple gateway kick user each other.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: add ex field to update group info.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* cicd: robot automated Change
* cicd: robot automated Change
* refactor: change project module name.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* refactor: change project module name.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* refactor: change project module name.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* cicd: robot automated Change
* test: for pressure test.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* test: for pressure test.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* test: for pressure test.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* test: message log.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* cicd: robot automated Change
* fxi: component check output valid info.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fxi: component check output valid info.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* test: send message test log.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* cicd: robot automated Change
* cicd: robot automated Change
* test: remove info log.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* feat: api of send message add sendTime field.
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
* fix: add callback for update user's info.
* cicd: robot automated Change
* fix: change callback command name.
* cicd: robot automated Change
* fix: single chat unread status change.
* fix: single chat unread status change.
* fix: single chat unread status change.
* fix: user status change.
* cicd: robot automated Change
* fix: user status change.
* fix: user status change.
* fix: user status change.
* cicd: robot automated Change
* fix: ws close when user logout.
* fix: remove repeat platform on online status.
* cicd: robot automated Change
* fix: api send messages for notification conversation .
* fix: api send messages for notification conversation .
* fix: api send messages for notification conversation .
* fix: api send messages for notification conversation .
* fix: api send messages for notification conversation .
* fix: api send messages for notification conversation.
* fix: api send messages for notification conversation.
* fix: api send messages for notification conversation.
* fix: api send messages for notification conversation.
* fix: api send messages for notification conversation.
* fix: api send messages for notification conversation.
* re: remove router of unsubscribeStatus.
* re: remove router of unsubscribeStatus.
* re: remove router of unsubscribeStatus.
* re: remove router of unsubscribeStatus.
* fix: reset branch
* fix: not support redis cluster. CROSSSLOT Keys in request don't hash to the same slot
* fix: update user.FaceURL do not trigger GroupMemberInfoSetNotification
* cicd: robot automated Change
* fix: api send messages for notification conversation.
* fix: api send messages for notification conversation.
* fix: zk add close to avoid zk block.
* fix: go mod update.
* fix: msg pull change and fcm redis flag fix.
---------
Signed-off-by: Gordon <1432970085@qq.com>
Signed-off-by: withchao <993506633@qq.com>
Signed-off-by: Gordon <46924906+FGadvancer@users.noreply.github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: withchao <993506633@qq.com>
Co-authored-by: Xinwei Xiong <3293172751NSS@gmail.com>
Co-authored-by: FGadvancer
Co-authored-by: withchao
---
internal/api/msg.go | 2 +-
pkg/common/db/cache/msg.go | 8 ++---
pkg/common/db/controller/msg.go | 53 ++++++++++++++++++++++++++++-----
3 files changed, 50 insertions(+), 13 deletions(-)
diff --git a/internal/api/msg.go b/internal/api/msg.go
index 028d77ca4..38e207cfb 100644
--- a/internal/api/msg.go
+++ b/internal/api/msg.go
@@ -53,7 +53,7 @@ func (MessageApi) SetOptions(options map[string]bool, value bool) {
utils.SetSwitchFromOptions(options, constant.IsConversationUpdate, value)
}
-func (m MessageApi) newUserSendMsgReq(c *gin.Context, params *apistruct.SendMsg) *msg.SendMsgReq {
+func (m MessageApi) newUserSendMsgReq(_ *gin.Context, params *apistruct.SendMsg) *msg.SendMsgReq {
var newContent string
options := make(map[string]bool, 5)
switch params.ContentType {
diff --git a/pkg/common/db/cache/msg.go b/pkg/common/db/cache/msg.go
index 2c869befb..50fb617aa 100644
--- a/pkg/common/db/cache/msg.go
+++ b/pkg/common/db/cache/msg.go
@@ -51,7 +51,7 @@ const (
getuiTaskID = "GETUI_TASK_ID"
signalCache = "SIGNAL_CACHE:"
signalListCache = "SIGNAL_LIST_CACHE:"
- fcmToken = "FCM_TOKEN:"
+ FCM_TOKEN = "FCM_TOKEN:"
messageCache = "MESSAGE_CACHE:"
messageDelUserList = "MESSAGE_DEL_USER_LIST:"
@@ -650,15 +650,15 @@ func (c *msgCache) GetSendMsgStatus(ctx context.Context, id string) (int32, erro
}
func (c *msgCache) SetFcmToken(ctx context.Context, account string, platformID int, fcmToken string, expireTime int64) (err error) {
- return errs.Wrap(c.rdb.Set(ctx, fcmToken+account+":"+strconv.Itoa(platformID), fcmToken, time.Duration(expireTime)*time.Second).Err())
+ return errs.Wrap(c.rdb.Set(ctx, FCM_TOKEN+account+":"+strconv.Itoa(platformID), fcmToken, time.Duration(expireTime)*time.Second).Err())
}
func (c *msgCache) GetFcmToken(ctx context.Context, account string, platformID int) (string, error) {
- return utils.Wrap2(c.rdb.Get(ctx, fcmToken+account+":"+strconv.Itoa(platformID)).Result())
+ return utils.Wrap2(c.rdb.Get(ctx, FCM_TOKEN+account+":"+strconv.Itoa(platformID)).Result())
}
func (c *msgCache) DelFcmToken(ctx context.Context, account string, platformID int) error {
- return errs.Wrap(c.rdb.Del(ctx, fcmToken+account+":"+strconv.Itoa(platformID)).Err())
+ return errs.Wrap(c.rdb.Del(ctx, FCM_TOKEN+account+":"+strconv.Itoa(platformID)).Err())
}
func (c *msgCache) IncrUserBadgeUnreadCountSum(ctx context.Context, userID string) (int, error) {
diff --git a/pkg/common/db/controller/msg.go b/pkg/common/db/controller/msg.go
index e3b6559dc..62ecf7232 100644
--- a/pkg/common/db/controller/msg.go
+++ b/pkg/common/db/controller/msg.go
@@ -436,6 +436,25 @@ func (db *commonMsgDatabase) getMsgBySeqsRange(ctx context.Context, userID strin
return seqMsgs, nil
}
+// GetMsgBySeqsRange In the context of group chat, we have the following parameters:
+//
+// "maxSeq" of a conversation: It represents the maximum value of messages in the group conversation.
+// "minSeq" of a conversation (default: 1): It represents the minimum value of messages in the group conversation.
+//
+// For a user's perspective regarding the group conversation, we have the following parameters:
+//
+// "userMaxSeq": It represents the user's upper limit for message retrieval in the group. If not set (default: 0),
+// it means the upper limit is the same as the conversation's "maxSeq".
+// "userMinSeq": It represents the user's starting point for message retrieval in the group. If not set (default: 0),
+// it means the starting point is the same as the conversation's "minSeq".
+//
+// The scenarios for these parameters are as follows:
+//
+// For users who have been kicked out of the group, "userMaxSeq" can be set as the maximum value they had before
+// being kicked out. This limits their ability to retrieve messages up to a certain point.
+// For new users joining the group, if they don't need to receive old messages,
+// "userMinSeq" can be set as the same value as the conversation's "maxSeq" at the moment they join the group.
+// This ensures that their message retrieval starts from the point they joined.
func (db *commonMsgDatabase) GetMsgBySeqsRange(ctx context.Context, userID string, conversationID string, begin, end, num, userMaxSeq int64) (int64, int64, []*sdkws.MsgData, error) {
userMinSeq, err := db.cache.GetConversationUserMinSeq(ctx, conversationID, userID)
if err != nil && errs.Unwrap(err) != redis.Nil {
@@ -448,6 +467,7 @@ func (db *commonMsgDatabase) GetMsgBySeqsRange(ctx context.Context, userID strin
if userMinSeq > minSeq {
minSeq = userMinSeq
}
+ //"minSeq" represents the startSeq value that the user can retrieve.
if minSeq > end {
log.ZInfo(ctx, "minSeq > end", "minSeq", minSeq, "end", end)
return 0, 0, nil, nil
@@ -462,23 +482,41 @@ func (db *commonMsgDatabase) GetMsgBySeqsRange(ctx context.Context, userID strin
maxSeq = userMaxSeq
}
}
+ //"maxSeq" represents the endSeq value that the user can retrieve.
+
if begin < minSeq {
begin = minSeq
}
if end > maxSeq {
end = maxSeq
}
+ //"begin" and "end" represent the actual startSeq and endSeq values that the user can retrieve.
if end < begin {
return 0, 0, nil, errs.ErrArgs.Wrap("seq end < begin")
}
var seqs []int64
- for i := end; i > end-num; i-- {
- if i >= begin {
- seqs = append([]int64{i}, seqs...)
- } else {
- break
+ if end-begin+1 <= num {
+ for i := begin; i <= end; i++ {
+ seqs = append(seqs, i)
}
- }
+ } else {
+ for i := end - num + 1; i <= end; i++ {
+ seqs = append(seqs, i)
+ }
+ }
+
+ //167 178 10
+ //if end-num < {
+ //
+ //}
+ //var seqs []int64
+ //for i := end; i > end-num; i-- {
+ // if i >= begin {
+ // seqs = append([]int64{i}, seqs...)
+ // } else {
+ // break
+ // }
+ //}
if len(seqs) == 0 {
return 0, 0, nil, nil
}
@@ -545,8 +583,7 @@ func (db *commonMsgDatabase) GetMsgBySeqsRange(ctx context.Context, userID strin
return 0, 0, nil, err
}
-
- successMsgs = append(successMsgs, mongoMsgs...)
+ successMsgs = append(mongoMsgs, successMsgs...)
}
return minSeq, maxSeq, successMsgs, nil
From b562da5c3776ef6bc42658a1d090bd577c434f1f Mon Sep 17 00:00:00 2001
From: Gordon <46924906+FGadvancer@users.noreply.github.com>
Date: Wed, 8 Nov 2023 18:17:09 +0800
Subject: [PATCH 04/15] fix: sync close ws conn when kick old user avoid wrong
trigger order about online status. (#1368)
---
internal/msggateway/client.go | 4 +++-
internal/msggateway/n_ws_server.go | 7 +------
2 files changed, 4 insertions(+), 7 deletions(-)
diff --git a/internal/msggateway/client.go b/internal/msggateway/client.go
index b32130c9a..9eeac3835 100644
--- a/internal/msggateway/client.go
+++ b/internal/msggateway/client.go
@@ -297,7 +297,9 @@ func (c *Client) KickOnlineMessage() error {
resp := Resp{
ReqIdentifier: WSKickOnlineMsg,
}
- return c.writeBinaryMsg(resp)
+ err := c.writeBinaryMsg(resp)
+ c.close()
+ return err
}
func (c *Client) writeBinaryMsg(resp Resp) error {
diff --git a/internal/msggateway/n_ws_server.go b/internal/msggateway/n_ws_server.go
index ad56c1373..c58710e1b 100644
--- a/internal/msggateway/n_ws_server.go
+++ b/internal/msggateway/n_ws_server.go
@@ -225,12 +225,7 @@ func (ws *WsServer) registerClient(client *Client) {
ws.onlineUserNum.Add(1)
ws.onlineUserConnNum.Add(1)
} else {
- i := &kickHandler{
- clientOK: clientOK,
- oldClients: oldClients,
- newClient: client,
- }
- ws.kickHandlerChan <- i
+ ws.multiTerminalLoginChecker(clientOK, oldClients, client)
log.ZDebug(client.ctx, "user exist", "userID", client.UserID, "platformID", client.PlatformID)
if clientOK {
ws.clients.Set(client.UserID, client)
From 9d542edc963a1d084969db5755a0f582d2508ec6 Mon Sep 17 00:00:00 2001
From: skiffer-git <72860476+skiffer-git@users.noreply.github.com>
Date: Fri, 10 Nov 2023 08:44:36 +0800
Subject: [PATCH 05/15] Update README-zh_CN.md
---
README-zh_CN.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/README-zh_CN.md b/README-zh_CN.md
index f24ee4739..699fdc476 100644
--- a/README-zh_CN.md
+++ b/README-zh_CN.md
@@ -29,6 +29,10 @@
+## 🟢 扫描微信进群交流
+
+
+
## Ⓜ️ 关于 OpenIM
OpenIM 不仅仅是一个开源的即时消息组件,它是你的应用程序生态系统的一个不可或缺的部分。查看下面的图表,了解 AppServer、AppClient、OpenIMServer 和 OpenIMSDK 是如何交互的。
From 4eb8e0068d6a93299497d31f06e8c9d7f27ad6fc Mon Sep 17 00:00:00 2001
From: skiffer-git <72860476+skiffer-git@users.noreply.github.com>
Date: Fri, 10 Nov 2023 08:46:00 +0800
Subject: [PATCH 06/15] Update README-zh_CN.md
---
README-zh_CN.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README-zh_CN.md b/README-zh_CN.md
index 699fdc476..db1e8beab 100644
--- a/README-zh_CN.md
+++ b/README-zh_CN.md
@@ -30,7 +30,7 @@
## 🟢 扫描微信进群交流
-
+
## Ⓜ️ 关于 OpenIM
From e40aca81dc753adc80f2d8e998e58db54bd75b08 Mon Sep 17 00:00:00 2001
From: chao <48119764+withchao@users.noreply.github.com>
Date: Thu, 9 Nov 2023 21:30:33 -0600
Subject: [PATCH 07/15] fix: GetUserReqApplicationList error when there is a
disbanded group chat (#1374)
---
internal/rpc/group/group.go | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go
index 17d748920..6a0ce5733 100644
--- a/internal/rpc/group/group.go
+++ b/internal/rpc/group/group.go
@@ -1126,9 +1126,6 @@ func (s *groupServer) GetUserReqApplicationList(ctx context.Context, req *pbgrou
groupMap := utils.SliceToMap(groups, func(e *relationtb.GroupModel) string {
return e.GroupID
})
- if ids := utils.Single(groupIDs, utils.Keys(groupMap)); len(ids) > 0 {
- return nil, errs.ErrGroupIDNotFound.Wrap(strings.Join(ids, ","))
- }
owners, err := s.FindGroupMember(ctx, groupIDs, nil, []int32{constant.GroupOwner})
if err != nil {
return nil, err
@@ -1144,7 +1141,11 @@ func (s *groupServer) GetUserReqApplicationList(ctx context.Context, req *pbgrou
return nil, err
}
resp.GroupRequests = utils.Slice(requests, func(e *relationtb.GroupRequestModel) *sdkws.GroupRequest {
- return convert.Db2PbGroupRequest(e, user, convert.Db2PbGroupInfo(groupMap[e.GroupID], ownerMap[e.GroupID].UserID, uint32(groupMemberNum[e.GroupID])))
+ var ownerUserID string
+ if owner, ok := ownerMap[e.GroupID]; ok {
+ ownerUserID = owner.UserID
+ }
+ return convert.Db2PbGroupRequest(e, user, convert.Db2PbGroupInfo(groupMap[e.GroupID], ownerUserID, groupMemberNum[e.GroupID]))
})
return resp, nil
}
From a285f02631feb12f430157d1b85bb908e9eb2dac Mon Sep 17 00:00:00 2001
From: chao <48119764+withchao@users.noreply.github.com>
Date: Fri, 10 Nov 2023 01:44:28 -0600
Subject: [PATCH 08/15] fix: error when querying some information about
disbanded group (#1376)
* fix: GetUserReqApplicationList error when there is a disbanded group chat
* fix: error when querying some information about disbanded group
---
internal/rpc/group/group.go | 32 +++++++++++++++++++-------------
1 file changed, 19 insertions(+), 13 deletions(-)
diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go
index 6a0ce5733..2a79c5d93 100644
--- a/internal/rpc/group/group.go
+++ b/internal/rpc/group/group.go
@@ -690,7 +690,11 @@ func (s *groupServer) GetGroupApplicationList(ctx context.Context, req *pbgroup.
return e.GroupID
})
resp.GroupRequests = utils.Slice(groupRequests, func(e *relationtb.GroupRequestModel) *sdkws.GroupRequest {
- return convert.Db2PbGroupRequest(e, userMap[e.UserID], convert.Db2PbGroupInfo(groupMap[e.GroupID], ownerMap[e.GroupID].UserID, groupMemberNumMap[e.GroupID]))
+ var ownerUserID string
+ if owner, ok := ownerMap[e.GroupID]; ok {
+ ownerUserID = owner.UserID
+ }
+ return convert.Db2PbGroupRequest(e, userMap[e.UserID], convert.Db2PbGroupInfo(groupMap[e.GroupID], ownerUserID, groupMemberNumMap[e.GroupID]))
})
return resp, nil
}
@@ -1056,16 +1060,20 @@ func (s *groupServer) GetGroups(ctx context.Context, req *pbgroup.GetGroupsReq)
ownerMemberMap := utils.SliceToMap(ownerMembers, func(e *relationtb.GroupMemberModel) string {
return e.GroupID
})
- if ids := utils.Single(groupIDs, utils.Keys(ownerMemberMap)); len(ids) > 0 {
- return nil, errs.ErrDatabase.Wrap("group not owner " + strings.Join(ids, ","))
- }
groupMemberNumMap, err := s.GroupDatabase.MapGroupMemberNum(ctx, groupIDs)
if err != nil {
return nil, err
}
resp.Groups = utils.Slice(groups, func(group *relationtb.GroupModel) *pbgroup.CMSGroup {
- member := ownerMemberMap[group.GroupID]
- return convert.Db2PbCMSGroup(group, member.UserID, member.Nickname, uint32(groupMemberNumMap[group.GroupID]))
+ var (
+ userID string
+ username string
+ )
+ if member, ok := ownerMemberMap[group.GroupID]; ok {
+ userID = member.UserID
+ username = member.Nickname
+ }
+ return convert.Db2PbCMSGroup(group, userID, username, groupMemberNumMap[group.GroupID])
})
return resp, nil
}
@@ -1133,9 +1141,6 @@ func (s *groupServer) GetUserReqApplicationList(ctx context.Context, req *pbgrou
ownerMap := utils.SliceToMap(owners, func(e *relationtb.GroupMemberModel) string {
return e.GroupID
})
- if ids := utils.Single(groupIDs, utils.Keys(ownerMap)); len(ids) > 0 {
- return nil, errs.ErrData.Wrap("group no owner", strings.Join(ids, ","))
- }
groupMemberNum, err := s.GroupDatabase.MapGroupMemberNum(ctx, groupIDs)
if err != nil {
return nil, err
@@ -1564,15 +1569,16 @@ func (s *groupServer) GetGroupUsersReqApplicationList(ctx context.Context, req *
ownerMap := utils.SliceToMap(owners, func(e *relationtb.GroupMemberModel) string {
return e.GroupID
})
- if ids := utils.Single(groupIDs, utils.Keys(ownerMap)); len(ids) > 0 {
- return nil, errs.ErrData.Wrap("group no owner", strings.Join(ids, ","))
- }
groupMemberNum, err := s.GroupDatabase.MapGroupMemberNum(ctx, groupIDs)
if err != nil {
return nil, err
}
resp.GroupRequests = utils.Slice(requests, func(e *relationtb.GroupRequestModel) *sdkws.GroupRequest {
- return convert.Db2PbGroupRequest(e, nil, convert.Db2PbGroupInfo(groupMap[e.GroupID], ownerMap[e.GroupID].UserID, uint32(groupMemberNum[e.GroupID])))
+ var ownerUserID string
+ if owner, ok := ownerMap[e.GroupID]; ok {
+ ownerUserID = owner.UserID
+ }
+ return convert.Db2PbGroupRequest(e, nil, convert.Db2PbGroupInfo(groupMap[e.GroupID], ownerUserID, groupMemberNum[e.GroupID]))
})
resp.Total = total
return resp, nil
From a32e94b5ae927ba819d63aff8a3e89710be619d2 Mon Sep 17 00:00:00 2001
From: chao <48119764+withchao@users.noreply.github.com>
Date: Fri, 10 Nov 2023 02:39:16 -0600
Subject: [PATCH 09/15] fix: GetUserReqApplicationList dismissed group error
(#1378)
* fix: GetUserReqApplicationList error when there is a disbanded group chat
* fix: error when querying some information about disbanded group
* fix: GetUserReqApplicationList dismissed group error
---
internal/rpc/group/group.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go
index 2a79c5d93..85b78cfb2 100644
--- a/internal/rpc/group/group.go
+++ b/internal/rpc/group/group.go
@@ -1127,7 +1127,7 @@ func (s *groupServer) GetUserReqApplicationList(ctx context.Context, req *pbgrou
groupIDs := utils.Distinct(utils.Slice(requests, func(e *relationtb.GroupRequestModel) string {
return e.GroupID
}))
- groups, err := s.GroupDatabase.FindNotDismissedGroup(ctx, groupIDs)
+ groups, err := s.GroupDatabase.FindGroup(ctx, groupIDs)
if err != nil {
return nil, err
}
From 686fa80800d54ab7992359d60c33e392c7790ba7 Mon Sep 17 00:00:00 2001
From: "fengyun.rui"
Date: Fri, 10 Nov 2023 17:55:50 +0800
Subject: [PATCH 10/15] refactor: lower the level of code nesting (#1370)
* refactor: lower the level of code nesting
Signed-off-by: rfyiamcool
* refactor: lower the level of code nesting
Signed-off-by: rfyiamcool
---------
Signed-off-by: rfyiamcool
---
internal/push/push_to_client.go | 107 +++++++++++++++++++-------------
1 file changed, 64 insertions(+), 43 deletions(-)
diff --git a/internal/push/push_to_client.go b/internal/push/push_to_client.go
index 2f3156c28..2d2c647ac 100644
--- a/internal/push/push_to_client.go
+++ b/internal/push/push_to_client.go
@@ -18,14 +18,9 @@ import (
"context"
"encoding/json"
"errors"
- "github.com/openimsdk/open-im-server/v3/pkg/common/prom_metrics"
- "github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/dummy"
-
- "github.com/OpenIMSDK/protocol/conversation"
-
- "github.com/openimsdk/open-im-server/v3/pkg/msgprocessor"
"github.com/OpenIMSDK/protocol/constant"
+ "github.com/OpenIMSDK/protocol/conversation"
"github.com/OpenIMSDK/protocol/msggateway"
"github.com/OpenIMSDK/protocol/sdkws"
"github.com/OpenIMSDK/tools/discoveryregistry"
@@ -34,6 +29,7 @@ import (
"github.com/OpenIMSDK/tools/utils"
"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush"
+ "github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/dummy"
"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/fcm"
"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/getui"
"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/jpush"
@@ -41,6 +37,8 @@ import (
"github.com/openimsdk/open-im-server/v3/pkg/common/db/cache"
"github.com/openimsdk/open-im-server/v3/pkg/common/db/controller"
"github.com/openimsdk/open-im-server/v3/pkg/common/db/localcache"
+ "github.com/openimsdk/open-im-server/v3/pkg/common/prom_metrics"
+ "github.com/openimsdk/open-im-server/v3/pkg/msgprocessor"
"github.com/openimsdk/open-im-server/v3/pkg/rpcclient"
)
@@ -53,7 +51,6 @@ type Pusher struct {
msgRpcClient *rpcclient.MessageRpcClient
conversationRpcClient *rpcclient.ConversationRpcClient
groupRpcClient *rpcclient.GroupRpcClient
- successCount int
}
var errNoOfflinePusher = errors.New("no offlinePusher is configured")
@@ -104,24 +101,29 @@ func (p *Pusher) Push2User(ctx context.Context, userIDs []string, msg *sdkws.Msg
if err := callbackOnlinePush(ctx, userIDs, msg); err != nil {
return err
}
+
// push
wsResults, err := p.GetConnsAndOnlinePush(ctx, msg, userIDs)
if err != nil {
return err
}
+
isOfflinePush := utils.GetSwitchFromOptions(msg.Options, constant.IsOfflinePush)
log.ZDebug(ctx, "push_result", "ws push result", wsResults, "sendData", msg, "isOfflinePush", isOfflinePush, "push_to_userID", userIDs)
- p.successCount++
- if isOfflinePush {
- for _, v := range wsResults {
- if msg.SendID != v.UserID && (!v.OnlinePush) {
- if err := callbackOfflinePush(ctx, userIDs, msg, &[]string{}); err != nil {
- return err
- }
- err = p.offlinePushMsg(ctx, msg.SendID, msg, []string{v.UserID})
- if err != nil {
- return err
- }
+
+ if !isOfflinePush {
+ return nil
+ }
+
+ for _, v := range wsResults {
+ if msg.SendID != v.UserID && (!v.OnlinePush) {
+ if err = callbackOfflinePush(ctx, userIDs, msg, &[]string{}); err != nil {
+ return err
+ }
+
+ err = p.offlinePushMsg(ctx, msg.SendID, msg, []string{v.UserID})
+ if err != nil {
+ return err
}
}
}
@@ -140,14 +142,16 @@ func (p *Pusher) UnmarshalNotificationElem(bytes []byte, t interface{}) error {
func (p *Pusher) Push2SuperGroup(ctx context.Context, groupID string, msg *sdkws.MsgData) (err error) {
log.ZDebug(ctx, "Get super group msg from msg_transfer and push msg", "msg", msg.String(), "groupID", groupID)
var pushToUserIDs []string
- if err := callbackBeforeSuperGroupOnlinePush(ctx, groupID, msg, &pushToUserIDs); err != nil {
+ if err = callbackBeforeSuperGroupOnlinePush(ctx, groupID, msg, &pushToUserIDs); err != nil {
return err
}
+
if len(pushToUserIDs) == 0 {
pushToUserIDs, err = p.groupLocalCache.GetGroupMemberIDs(ctx, groupID)
if err != nil {
return err
}
+
switch msg.ContentType {
case constant.MemberQuitNotification:
var tips sdkws.MemberQuitTips
@@ -155,7 +159,7 @@ func (p *Pusher) Push2SuperGroup(ctx context.Context, groupID string, msg *sdkws
return err
}
defer func(groupID string, userIDs []string) {
- if err := p.DeleteMemberAndSetConversationSeq(ctx, groupID, userIDs); err != nil {
+ if err = p.DeleteMemberAndSetConversationSeq(ctx, groupID, userIDs); err != nil {
log.ZError(ctx, "MemberQuitNotification DeleteMemberAndSetConversationSeq", err, "groupID", groupID, "userIDs", userIDs)
}
}(groupID, []string{tips.QuitUser.UserID})
@@ -167,7 +171,7 @@ func (p *Pusher) Push2SuperGroup(ctx context.Context, groupID string, msg *sdkws
}
kickedUsers := utils.Slice(tips.KickedUserList, func(e *sdkws.GroupMemberFullInfo) string { return e.UserID })
defer func(groupID string, userIDs []string) {
- if err := p.DeleteMemberAndSetConversationSeq(ctx, groupID, userIDs); err != nil {
+ if err = p.DeleteMemberAndSetConversationSeq(ctx, groupID, userIDs); err != nil {
log.ZError(ctx, "MemberKickedNotification DeleteMemberAndSetConversationSeq", err, "groupID", groupID, "userIDs", userIDs)
}
}(groupID, kickedUsers)
@@ -183,48 +187,61 @@ func (p *Pusher) Push2SuperGroup(ctx context.Context, groupID string, msg *sdkws
ctx = mcontext.WithOpUserIDContext(ctx, config.Config.Manager.UserID[0])
}
defer func(groupID string) {
- if err := p.groupRpcClient.DismissGroup(ctx, groupID); err != nil {
+ if err = p.groupRpcClient.DismissGroup(ctx, groupID); err != nil {
log.ZError(ctx, "DismissGroup Notification clear members", err, "groupID", groupID)
}
}(groupID)
}
}
}
+
wsResults, err := p.GetConnsAndOnlinePush(ctx, msg, pushToUserIDs)
if err != nil {
return err
}
+
log.ZDebug(ctx, "get conn and online push success", "result", wsResults, "msg", msg)
- p.successCount++
isOfflinePush := utils.GetSwitchFromOptions(msg.Options, constant.IsOfflinePush)
if isOfflinePush {
- var onlineSuccessUserIDs []string
- var WebAndPcBackgroundUserIDs []string
- onlineSuccessUserIDs = append(onlineSuccessUserIDs, msg.SendID)
+ var (
+ onlineSuccessUserIDs = []string{msg.SendID}
+ webAndPcBackgroundUserIDs []string
+ )
+
for _, v := range wsResults {
if v.OnlinePush && v.UserID != msg.SendID {
onlineSuccessUserIDs = append(onlineSuccessUserIDs, v.UserID)
}
- if !v.OnlinePush {
- if len(v.Resp) != 0 {
- for _, singleResult := range v.Resp {
- if singleResult.ResultCode == -2 {
- if constant.PlatformIDToName(int(singleResult.RecvPlatFormID)) == constant.TerminalPC ||
- singleResult.RecvPlatFormID == constant.WebPlatformID {
- WebAndPcBackgroundUserIDs = append(WebAndPcBackgroundUserIDs, v.UserID)
- }
- }
- }
+
+ if v.OnlinePush {
+ continue
+ }
+
+ if len(v.Resp) == 0 {
+ continue
+ }
+
+ for _, singleResult := range v.Resp {
+ if singleResult.ResultCode != -2 {
+ continue
+ }
+
+ isPC := constant.PlatformIDToName(int(singleResult.RecvPlatFormID)) == constant.TerminalPC
+ isWebID := singleResult.RecvPlatFormID == constant.WebPlatformID
+
+ if isPC || isWebID {
+ webAndPcBackgroundUserIDs = append(webAndPcBackgroundUserIDs, v.UserID)
}
}
}
+
needOfflinePushUserIDs := utils.DifferenceString(onlineSuccessUserIDs, pushToUserIDs)
if msg.ContentType != constant.SignalingNotification {
notNotificationUserIDs, err := p.conversationLocalCache.GetRecvMsgNotNotifyUserIDs(ctx, groupID)
if err != nil {
- // log.ZError(ctx, "GetRecvMsgNotNotifyUserIDs failed", err, "groupID", groupID)
return err
}
+
needOfflinePushUserIDs = utils.SliceSub(needOfflinePushUserIDs, notNotificationUserIDs)
}
// Use offline push messaging
@@ -234,6 +251,7 @@ func (p *Pusher) Push2SuperGroup(ctx context.Context, groupID string, msg *sdkws
if err != nil {
return err
}
+
if len(offlinePushUserIDs) > 0 {
needOfflinePushUserIDs = offlinePushUserIDs
}
@@ -250,8 +268,8 @@ func (p *Pusher) Push2SuperGroup(ctx context.Context, groupID string, msg *sdkws
log.ZError(ctx, "offlinePushMsg failed", err, "groupID", groupID, "msg", msg)
return err
}
- if _, err := p.GetConnsAndOnlinePush(ctx, msg, utils.IntersectString(resp.UserIDs, WebAndPcBackgroundUserIDs)); err != nil {
- log.ZError(ctx, "offlinePushMsg failed", err, "groupID", groupID, "msg", msg, "userIDs", utils.IntersectString(needOfflinePushUserIDs, WebAndPcBackgroundUserIDs))
+ if _, err := p.GetConnsAndOnlinePush(ctx, msg, utils.IntersectString(resp.UserIDs, webAndPcBackgroundUserIDs)); err != nil {
+ log.ZError(ctx, "offlinePushMsg failed", err, "groupID", groupID, "msg", msg, "userIDs", utils.IntersectString(needOfflinePushUserIDs, webAndPcBackgroundUserIDs))
return err
}
}
@@ -319,15 +337,18 @@ func (p *Pusher) getOfflinePushInfos(conversationID string, msg *sdkws.MsgData)
err = errNoOfflinePusher
return
}
- type AtContent struct {
+
+ type atContent struct {
Text string `json:"text"`
AtUserList []string `json:"atUserList"`
IsAtSelf bool `json:"isAtSelf"`
}
+
opts, err = p.GetOfflinePushOpts(msg)
if err != nil {
return
}
+
if msg.OfflinePushInfo != nil {
title = msg.OfflinePushInfo.Title
content = msg.OfflinePushInfo.Desc
@@ -345,9 +366,9 @@ func (p *Pusher) getOfflinePushInfos(conversationID string, msg *sdkws.MsgData)
case constant.File:
title = constant.ContentType2PushContent[int64(msg.ContentType)]
case constant.AtText:
- a := AtContent{}
- _ = utils.JsonStringToStruct(string(msg.Content), &a)
- if utils.IsContain(conversationID, a.AtUserList) {
+ ac := atContent{}
+ _ = utils.JsonStringToStruct(string(msg.Content), &ac)
+ if utils.IsContain(conversationID, ac.AtUserList) {
title = constant.ContentType2PushContent[constant.AtText] + constant.ContentType2PushContent[constant.Common]
} else {
title = constant.ContentType2PushContent[constant.GroupMsg]
From e2004c1e9df5e6f9c3f12e217568cb133f49564c Mon Sep 17 00:00:00 2001
From: Xinwei Xiong <3293172751@qq.com>
Date: Fri, 10 Nov 2023 19:37:25 +0800
Subject: [PATCH 11/15] =?UTF-8?q?=E2=98=80=EF=B8=8F=20feat:=20Enhancing=20?=
=?UTF-8?q?OpenIM=20with=20Integrated=20E2E=20Testing=20and=20CI/CD=20Enha?=
=?UTF-8?q?ncements=20(#1359)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* cicd: robot automated Change
* feat: add api test
Signed-off-by: Xinwei Xiong(cubxxw) <3293172751nss@gmail.com>
* feat: add api test make file
Signed-off-by: Xinwei Xiong(cubxxw) <3293172751nss@gmail.com>
* feat: add openim e2e test
Signed-off-by: Xinwei Xiong(cubxxw) <3293172751nss@gmail.com>
* feat: add openim e2e test
Signed-off-by: Xinwei Xiong(cubxxw) <3293172751nss@gmail.com>
* fix: Fixed some unused scripts and some names
Signed-off-by: Xinwei Xiong(cubxxw) <3293172751nss@gmail.com>
* docs: optimize openim docs
Signed-off-by: Xinwei Xiong(cubxxw) <3293172751nss@gmail.com>
* feat: add prom address
Signed-off-by: Xinwei Xiong(cubxxw) <3293172751nss@gmail.com>
* feat: add openim info test
* feat: add openim images config path
Signed-off-by: Xinwei Xiong(cubxxw) <3293172751nss@gmail.com>
* fix: fix tim file rename
* fix: fix tim file rename
* fix: fix tim file rename
* fix: fix tim file rename
* fix: add openim test e2e
* feat: add openim test .keep
Signed-off-by: Xinwei Xiong(cubxxw) <3293172751nss@gmail.com>
* feat: add openim test .keep
Signed-off-by: Xinwei Xiong(cubxxw) <3293172751nss@gmail.com>
* feat: openim test
Signed-off-by: Xinwei Xiong(cubxxw) <3293172751nss@gmail.com>
* feat: openim test
Signed-off-by: Xinwei Xiong(cubxxw) <3293172751nss@gmail.com>
* feat: openim test
Signed-off-by: Xinwei Xiong(cubxxw) <3293172751nss@gmail.com>
---------
Signed-off-by: Xinwei Xiong(cubxxw) <3293172751nss@gmail.com>
Co-authored-by: cubxxw
---
.env | 4 +-
.github/.codecov.yml | 18 +-
.github/workflows/api-test.yml | 90 ++
.github/workflows/create_branch_on_tag.yml | 14 +
.github/workflows/e2e-test.yml | 60 +-
.github/workflows/openimci.yml | 33 +-
CHANGELOG/CHANGELOG.md | 2 +-
CONTRIBUTING.md | 8 +-
Makefile | 11 +-
README-zh_CN.md | 14 +-
README.md | 81 +-
build/images/Dockerfile | 14 +
chat | Bin 0 -> 7068773 bytes
cmd/openim-api/main.go | 10 +-
config/config.yaml | 2 +-
deployments/README.md | 2 +-
.../openim-api/templates/deployment.yaml | 4 +-
.../templates/deployment.yaml | 18 +-
.../templates/deployment.yaml | 4 +-
.../openim-push/templates/deployment.yaml | 4 +-
.../openim-rpc-auth/templates/deployment.yaml | 4 +-
.../templates/deployment.yaml | 4 +-
.../templates/deployment.yaml | 4 +-
.../templates/deployment.yaml | 4 +-
.../openim-rpc-msg/templates/deployment.yaml | 4 +-
.../templates/deployment.yaml | 4 +-
.../openim-rpc-user/templates/deployment.yaml | 4 +-
docker-compose.yml | 68 +-
docs/README.md | 52 +-
docs/contrib/README.md | 42 +
docs/{conversions => contrib}/api.md | 0
.../bash_log.md => contrib/bash-log.md} | 0
.../cicd-actions.md} | 0
...ode_conventions.md => code-conventions.md} | 43 +-
docs/{conversions => contrib}/commit.md | 0
docs/{conversions => contrib}/directory.md | 0
docs/contrib/environment.md | 4 +-
.../error_code.md => contrib/error-code.md} | 0
.../{git_workflow.md => git-workflow.md} | 0
.../{git_cherry-pick.md => gitcherry-pick.md} | 0
.../github-workflow.md | 2 +-
.../go_code.md => contrib/go-code.md} | 12 +
docs/{conversions => contrib}/images.md | 0
.../{init_config.md => init-config.md} | 0
.../{install_docker.md => install-docker.md} | 0
...ux_development.md => linux-development.md} | 0
.../{local_actions.md => local-actions.md} | 0
docs/{conversions => contrib}/logging.md | 0
docs/contrib/offline-deployment.md | 4 +-
.../{protoc_tools.md => protoc-tools.md} | 0
docs/contrib/test.md | 183 +++
docs/contrib/{util_go.md => util-go.md} | 0
.../{util_makefile.md => util-makefile.md} | 0
.../{util_scripts.md => util-scripts.md} | 0
docs/{conversions => contrib}/version.md | 2 +-
docs/conversions/README.md | 10 -
go.mod | 3 +
go.sum | 4 +
install.sh | 15 +-
install_guide.sh | 2 +-
internal/msggateway/n_ws_server.go | 6 +-
internal/msgtransfer/init.go | 15 +-
.../online_msg_to_mongo_handler.go | 6 +-
internal/push/offlinepush/fcm/push_test.go | 32 -
internal/push/push_to_client.go | 7 +-
internal/rpc/auth/auth.go | 4 +-
internal/rpc/msg/send.go | 12 +-
internal/tools/conversation.go | 12 +-
internal/tools/msg.go | 11 +-
pkg/apistruct/manage.go | 79 +-
pkg/common/cmd/msg_gateway_test.go | 51 +
pkg/common/config/parse.go | 4 +-
pkg/common/config/parse_test.go | 117 ++
pkg/common/db/cache/meta_cache.go | 3 +-
pkg/common/db/cache/s3.go | 8 +-
pkg/common/db/controller/msg.go | 10 +-
pkg/common/db/controller/s3.go | 8 +-
pkg/common/db/relation/conversation_model.go | 1 +
pkg/common/db/s3/cont/controller.go | 3 +-
pkg/common/db/s3/minio/minio.go | 3 +-
pkg/common/db/s3/minio/thumbnail.go | 12 +-
.../discoveryregister.go} | 2 +-
.../discoveryregister_test.go | 407 +++++
.../ginprometheus.go} | 2 +-
pkg/common/http/http_client_test.go | 154 ++
pkg/common/locker/doc.go | 15 -
pkg/common/locker/message_locker.go | 72 -
.../gin-api.go => prommetrics/gin_api.go} | 4 +-
.../grpc-auth.go => prommetrics/grpc_auth.go} | 2 +-
.../grpc-msg.go => prommetrics/grpc_msg.go} | 2 +-
.../grpc_msggateway.go} | 2 +-
.../grpc_push.go | 2 +-
.../func.go => prommetrics/prommetrics.go} | 10 +-
pkg/common/prommetrics/prommetrics_test.go | 60 +
.../{prom_metrics => prommetrics}/transfer.go | 2 +-
pkg/common/startrpc/start.go | 15 +-
pkg/common/startrpc/start_test.go | 52 +
pkg/common/tls/tls.go | 1 +
pkg/msgprocessor/conversation_test.go | 334 ++++
scripts/README.md | 6 +-
scripts/batch_start_all.sh | 84 -
scripts/build.cmd | 13 -
scripts/cherry-pick.sh | 6 +-
scripts/{create_topic.sh => create-topic.sh} | 0
scripts/genconfig.sh | 5 +-
scripts/init-config.sh | 2 +-
scripts/{init_pwd.sh => init-pwd.sh} | 0
scripts/install/dependency.sh | 6 +-
scripts/install/environment.sh | 10 +-
scripts/install/install.sh | 9 +-
scripts/install/openim-api.sh | 3 +-
scripts/install/openim-crontask.sh | 12 +-
scripts/install/openim-msggateway.sh | 12 +-
scripts/install/openim-msgtransfer.sh | 15 +-
scripts/install/openim-push.sh | 12 +-
scripts/install/openim-rpc.sh | 9 +-
scripts/install/test.sh | 1394 ++++++++++++++---
scripts/install_im_compose.sh | 66 -
scripts/lib/color.sh | 3 +-
scripts/lib/logging.sh | 3 +-
scripts/lib/util.sh | 15 +
scripts/make-rules/golang.mk | 12 +
scripts/make-rules/image.mk | 2 +-
scripts/start.bat | 12 -
scripts/template/head.md.tmpl | 4 +-
scripts/wait-for-it.sh | 9 +-
test/e2e/README.md | 134 ++
test/e2e/api/.keep | 1 +
test/e2e/api/token/token.go | 138 ++
test/e2e/api/user/curd.go | 44 +
test/e2e/api/user/user.go | 101 ++
test/e2e/conformance/.keep | 1 +
test/e2e/e2e.go | 37 +
test/e2e/e2e_test.go | 23 +
test/e2e/framework/config/.keep | 1 +
test/e2e/framework/config/config.go | 21 +
test/e2e/framework/config/config_test.go | 75 +
test/e2e/framework/ginkgowrapper/.keep | 1 +
.../framework/ginkgowrapper/ginkgowrapper.go | 1 +
.../ginkgowrapper/ginkgowrapper_test.go | 1 +
test/e2e/framework/helpers/.keep | 1 +
test/e2e/framework/helpers/chat/chat.go | 152 ++
test/e2e/performance/.keep | 1 +
test/e2e/rpc/auth/.keep | 1 +
test/e2e/rpc/conversation/.keep | 1 +
test/e2e/rpc/friend/.keep | 1 +
test/e2e/rpc/group/.keep | 1 +
test/e2e/rpc/message/.keep | 1 +
test/e2e/scalability/.keep | 1 +
test/e2e/upgrade/.keep | 1 +
test/e2e/web/.keep | 1 +
test/readme | 2 +
test/typecheck/typecheck_test.go | 4 +-
test/wrktest.sh | 12 +-
154 files changed, 3992 insertions(+), 900 deletions(-)
create mode 100644 .github/workflows/api-test.yml
create mode 100755 chat
create mode 100644 docs/contrib/README.md
rename docs/{conversions => contrib}/api.md (100%)
rename docs/{conversions/bash_log.md => contrib/bash-log.md} (100%)
rename docs/{conversions/cicd_actions.md => contrib/cicd-actions.md} (100%)
rename docs/contrib/{code_conventions.md => code-conventions.md} (52%)
rename docs/{conversions => contrib}/commit.md (100%)
rename docs/{conversions => contrib}/directory.md (100%)
rename docs/{conversions/error_code.md => contrib/error-code.md} (100%)
rename docs/contrib/{git_workflow.md => git-workflow.md} (100%)
rename docs/contrib/{git_cherry-pick.md => gitcherry-pick.md} (100%)
rename docs/{conversions => contrib}/github-workflow.md (99%)
rename docs/{conversions/go_code.md => contrib/go-code.md} (96%)
rename docs/{conversions => contrib}/images.md (100%)
rename docs/contrib/{init_config.md => init-config.md} (100%)
rename docs/contrib/{install_docker.md => install-docker.md} (100%)
rename docs/contrib/{linux_development.md => linux-development.md} (100%)
rename docs/contrib/{local_actions.md => local-actions.md} (100%)
rename docs/{conversions => contrib}/logging.md (100%)
rename docs/contrib/{protoc_tools.md => protoc-tools.md} (100%)
create mode 100644 docs/contrib/test.md
rename docs/contrib/{util_go.md => util-go.md} (100%)
rename docs/contrib/{util_makefile.md => util-makefile.md} (100%)
rename docs/contrib/{util_scripts.md => util-scripts.md} (100%)
rename docs/{conversions => contrib}/version.md (99%)
delete mode 100644 docs/conversions/README.md
delete mode 100644 internal/push/offlinepush/fcm/push_test.go
create mode 100644 pkg/common/cmd/msg_gateway_test.go
create mode 100644 pkg/common/config/parse_test.go
rename pkg/common/{discovery_register/k8s_discovery_register.go => discoveryregister/discoveryregister.go} (98%)
create mode 100644 pkg/common/discoveryregister/discoveryregister_test.go
rename pkg/common/{ginPrometheus/ginPrometheus.go => ginprometheus/ginprometheus.go} (99%)
create mode 100644 pkg/common/http/http_client_test.go
delete mode 100644 pkg/common/locker/doc.go
delete mode 100644 pkg/common/locker/message_locker.go
rename pkg/common/{prom_metrics/gin-api.go => prommetrics/gin_api.go} (91%)
rename pkg/common/{prom_metrics/grpc-auth.go => prommetrics/grpc_auth.go} (90%)
rename pkg/common/{prom_metrics/grpc-msg.go => prommetrics/grpc_msg.go} (97%)
rename pkg/common/{prom_metrics/grpc-msggateway.go => prommetrics/grpc_msggateway.go} (90%)
rename pkg/common/{prom_metrics => prommetrics}/grpc_push.go (92%)
rename pkg/common/{prom_metrics/func.go => prommetrics/prommetrics.go} (87%)
create mode 100644 pkg/common/prommetrics/prommetrics_test.go
rename pkg/common/{prom_metrics => prommetrics}/transfer.go (97%)
create mode 100644 pkg/common/startrpc/start_test.go
create mode 100644 pkg/msgprocessor/conversation_test.go
delete mode 100755 scripts/batch_start_all.sh
delete mode 100644 scripts/build.cmd
rename scripts/{create_topic.sh => create-topic.sh} (100%)
rename scripts/{init_pwd.sh => init-pwd.sh} (100%)
delete mode 100755 scripts/install_im_compose.sh
delete mode 100644 scripts/start.bat
create mode 100644 test/e2e/README.md
create mode 100644 test/e2e/api/.keep
create mode 100644 test/e2e/api/token/token.go
create mode 100644 test/e2e/api/user/curd.go
create mode 100644 test/e2e/api/user/user.go
create mode 100644 test/e2e/conformance/.keep
create mode 100644 test/e2e/e2e.go
create mode 100644 test/e2e/e2e_test.go
create mode 100644 test/e2e/framework/config/.keep
create mode 100644 test/e2e/framework/config/config.go
create mode 100644 test/e2e/framework/config/config_test.go
create mode 100644 test/e2e/framework/ginkgowrapper/.keep
create mode 100644 test/e2e/framework/ginkgowrapper/ginkgowrapper.go
create mode 100644 test/e2e/framework/ginkgowrapper/ginkgowrapper_test.go
create mode 100644 test/e2e/framework/helpers/.keep
create mode 100644 test/e2e/framework/helpers/chat/chat.go
create mode 100644 test/e2e/performance/.keep
create mode 100644 test/e2e/rpc/auth/.keep
create mode 100644 test/e2e/rpc/conversation/.keep
create mode 100644 test/e2e/rpc/friend/.keep
create mode 100644 test/e2e/rpc/group/.keep
create mode 100644 test/e2e/rpc/message/.keep
create mode 100644 test/e2e/scalability/.keep
create mode 100644 test/e2e/upgrade/.keep
create mode 100644 test/e2e/web/.keep
diff --git a/.env b/.env
index bcfc0722a..cc42fbcb7 100644
--- a/.env
+++ b/.env
@@ -29,8 +29,8 @@ PASSWORD=openIM123
MINIO_ENDPOINT=http://172.28.0.1:10005
# Base URL for the application programming interface (API).
-# Default: API_URL=http://172.28.0.1:10002
-API_URL=http://172.28.0.1:10002
+# Default: API_URL=http://172.0.0.1:10002
+API_URL=http://172.0.0.1:10002
# Directory path for storing data files or related information.
# Default: DATA_DIR=./
diff --git a/.github/.codecov.yml b/.github/.codecov.yml
index 9e262e0e3..fab584a31 100644
--- a/.github/.codecov.yml
+++ b/.github/.codecov.yml
@@ -20,4 +20,20 @@ coverage:
paths:
- pkg/* # only include coverage in "pkg/" folder
informational: true # Always pass check
- patch: off # disable the commit only checks
\ No newline at end of file
+ tools: # declare a new status context "tools"
+ paths:
+ - tools/* # only include coverage in "tools/" folder
+ informational: true # Always pass check
+ test: # declare a new status context "test"
+ paths:
+ - test/* # only include coverage in "test/" folder
+ informational: true # Always pass check
+ # internal: # declare a new status context "internal"
+ # paths:
+ # - internal/* # only include coverage in "internal/" folder
+ # informational: true # Always pass check
+ # cmd: # declare a new status context "cmd"
+ # paths:
+ # - cmd/* # only include coverage in "cmd/" folder
+ # informational: true # Always pass check
+ patch: off # disable the commit only checks
diff --git a/.github/workflows/api-test.yml b/.github/workflows/api-test.yml
new file mode 100644
index 000000000..0bbc86619
--- /dev/null
+++ b/.github/workflows/api-test.yml
@@ -0,0 +1,90 @@
+# Copyright © 2023 OpenIM. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: OpenIM API TEST
+
+on:
+ push:
+ branches:
+ - main
+ paths-ignore:
+ - "docs/**"
+ - "README.md"
+ - "README_zh-CN.md"
+ - "CONTRIBUTING.md"
+ pull_request:
+ branches:
+ - main
+ paths-ignore:
+ - "README.md"
+ - "README_zh-CN.md"
+ - "CONTRIBUTING.md"
+ - "docs/**"
+
+env:
+ GO_VERSION: "1.19"
+ GOLANGCI_VERSION: "v1.50.1"
+
+jobs:
+ execute-linux-systemd-scripts:
+ name: Execute OpenIM script on ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ environment:
+ name: openim
+ strategy:
+ matrix:
+ go_version: ["1.20"]
+ os: ["ubuntu-latest"]
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Go ${{ matrix.go_version }}
+ uses: actions/setup-go@v4
+ with:
+ go-version: ${{ matrix.go_version }}
+ id: go
+
+ - name: Install Task
+ uses: arduino/setup-task@v1
+ with:
+ version: '3.x' # If available, use the latest major version that's compatible
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Docker Operations
+ run: |
+ curl -o docker-compose.yml https://raw.githubusercontent.com/OpenIMSDK/openim-docker/main/example/basic-openim-server-dependency.yml
+ sudo docker compose up -d
+ sudo sleep 60
+
+ - name: Module Operations
+ run: |
+ sudo make tidy
+ sudo make tools.verify.go-gitlint
+
+ - name: Build, Start, Check Services and Print Logs
+ run: |
+ sudo ./scripts/install/install.sh -i && \
+ sudo ./scripts/install/install.sh -s && \
+ (echo "An error occurred, printing logs:" && sudo cat ./_output/logs/* 2>/dev/null)
+
+ - name: Run Test
+ run: |
+ sudo make test-api && \
+ (echo "An error occurred, printing logs:" && sudo cat ./_output/logs/* 2>/dev/null)
+
+ - name: Stop Services
+ run: |
+ sudo ./scripts/install/install.sh -u && \
+ (echo "An error occurred, printing logs:" && sudo cat ./_output/logs/* 2>/dev/null)
\ No newline at end of file
diff --git a/.github/workflows/create_branch_on_tag.yml b/.github/workflows/create_branch_on_tag.yml
index 0d8b0d91a..b6b9eb8e8 100644
--- a/.github/workflows/create_branch_on_tag.yml
+++ b/.github/workflows/create_branch_on_tag.yml
@@ -1,3 +1,17 @@
+# Copyright © 2023 OpenIM. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
name: Create Branch on Tag
on:
diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml
index 1e5949edf..535c30f75 100644
--- a/.github/workflows/e2e-test.yml
+++ b/.github/workflows/e2e-test.yml
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-name: OpenIM E2E Test
+name: OpenIM Linux System E2E Test
on:
workflow_dispatch:
@@ -41,4 +41,60 @@ jobs:
- name: Create e2e test
run: |
- echo "...test e2e"
\ No newline at end of file
+ echo "...test e2e"
+
+ execute-linux-systemd-scripts:
+ name: Execute OpenIM script on ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ environment:
+ name: openim
+ strategy:
+ matrix:
+ go_version: ["1.20"]
+ os: ["ubuntu-latest"]
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Go ${{ matrix.go_version }}
+ uses: actions/setup-go@v4
+ with:
+ go-version: ${{ matrix.go_version }}
+ id: go
+
+ - name: Install Task
+ uses: arduino/setup-task@v1
+ with:
+ version: '3.x' # If available, use the latest major version that's compatible
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Docker Operations
+ run: |
+ curl -o docker-compose.yml https://raw.githubusercontent.com/OpenIMSDK/openim-docker/main/example/basic-openim-server-dependency.yml
+ sudo docker compose up -d
+ sudo sleep 60
+
+ - name: Module Operations
+ run: |
+ sudo make tidy
+ sudo make tools.verify.go-gitlint
+
+ - name: Build, Start
+ run: |
+ sudo ./scripts/install/install.sh -i
+
+ - name: Exec OpenIM System Status Chack
+ run: |
+ sudo ./scripts/install/install.sh -s
+
+ - name: Exec OpenIM API test
+ run: |
+ sudo make test-api
+
+ - name: Exec OpenIM E2E test
+ run: |
+ sudo make test-e2e
+
+ - name: Exec OpenIM System uninstall
+ run: |
+ sudo ./scripts/install/install.sh -u
\ No newline at end of file
diff --git a/.github/workflows/openimci.yml b/.github/workflows/openimci.yml
index 05a8ce5c5..2e4cc9e24 100644
--- a/.github/workflows/openimci.yml
+++ b/.github/workflows/openimci.yml
@@ -64,7 +64,8 @@ jobs:
- name: Install Task
uses: arduino/setup-task@v1
with:
- version: 2.x
+ version: '3.x' # If available, use the latest major version that's compatible
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Module Operations
run: |
@@ -122,7 +123,8 @@ jobs:
- name: Install Task
uses: arduino/setup-task@v1
with:
- version: 2.x
+ version: '3.x' # If available, use the latest major version that's compatible
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Run OpenIM make install start
run: |
sudo make install
@@ -145,10 +147,13 @@ jobs:
with:
go-version: ${{ matrix.go_version }}
id: go
+
- name: Install Task
uses: arduino/setup-task@v1
with:
- version: 2.x
+ version: '3.x' # If available, use the latest major version that's compatible
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+
- name: Docker Operations
run: |
curl -o docker-compose.yml https://raw.githubusercontent.com/OpenIMSDK/openim-docker/main/example/basic-openim-server-dependency.yml
@@ -160,18 +165,15 @@ jobs:
sudo make tidy
sudo make tools.verify.go-gitlint
- - name: Build, Start and Check Services
+ - name: Build, Start, Check Services and Print Logs
run: |
- sudo make init
- sudo make build
- sudo make start
- sudo make check
-
- - name: Print OpenIM Logs
- run: sudo cat ./_output/logs/* 2>/dev/null
- continue-on-error: true
+ sudo make init && \
+ sudo make build && \
+ sudo make start && \
+ sudo make check || \
+ (echo "An error occurred, printing logs:" && sudo cat ./_output/logs/* 2>/dev/null)
- openim-build-image:
+ openim-test-build-image:
name: Build OpenIM Docker Image
runs-on: ubuntu-latest
environment:
@@ -184,10 +186,13 @@ jobs:
with:
go-version: ${{ matrix.go_version }}
id: go
+
- name: Install Task
uses: arduino/setup-task@v1
with:
- version: 2.x
+ version: '3.x' # If available, use the latest major version that's compatible
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+
- name: Test Docker Build
run: |
sudo make image
\ No newline at end of file
diff --git a/CHANGELOG/CHANGELOG.md b/CHANGELOG/CHANGELOG.md
index 2358b5fb8..f5d62ab24 100644
--- a/CHANGELOG/CHANGELOG.md
+++ b/CHANGELOG/CHANGELOG.md
@@ -20,7 +20,7 @@ All notable changes to this project will be documented in this file.
## OpenIM versioning policy
-+ [OpenIM Version](../docs/conversions/version.md)
++ [OpenIM Version](../docs/contrib/version.md)
## command
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index afa8b35d4..ee275e7ad 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -42,6 +42,12 @@ If you are familiar with [Makefile](./Makefile) , you can easily see the clever
The [Makefile](./Makefile) is for every developer, even if you don't know how to use the Makefile tool, don't worry, we provide two great commands to get you up to speed with the Makefile architecture, `make help` and `make help-all`, it can reduce problems of the developing environment.
+In accordance with the naming conventions adopted by OpenIM and drawing reference from the Google Naming Conventions as per the guidelines available at https://google.github.io/styleguide/go/, the following expectations for naming practices within the project are set forth:
+
++ https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/code-conventions.md
++ https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/go-code.md
+
+
## Code of ConductCode of Conduct
#### Code and doc contribution
@@ -270,7 +276,7 @@ You can find some very formal PR in [RFC](https://github.com/openimsdk/open-im-s
**🈴 Reviewing PRs:**
+ Be respectful and constructive
-+ Assign yourself to the PR
++ Assign yourself to the PR (comment `/assign`)
+ Check if all checks are passing
+ Suggest changes instead of simply commenting on found issues
+ If you are unsure about something, ask the author
diff --git a/Makefile b/Makefile
index 67f595fb4..1941bde6c 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@
## all: Run tidy, gen, add-copyright, format, lint, cover, build ✨
.PHONY: all
-all: tidy gen add-copyright verify lint cover restart
+all: tidy gen add-copyright verify test-api lint cover restart
# ==============================================================================
# Build set
@@ -166,6 +166,15 @@ test:
cover:
@$(MAKE) go.test.cover
+## test-api: Run api test. ✨
+.PHONY: test-api
+test-api:
+ @$(MAKE) go.test.api
+
+## test-e2e: Run e2e test
+test-e2e:
+ @$(MAKE) go.test.e2e
+
## updates: Check for updates to go.mod dependencies. ✨
.PHONY: updates
@$(MAKE) go.updates
diff --git a/README-zh_CN.md b/README-zh_CN.md
index db1e8beab..6dd264342 100644
--- a/README-zh_CN.md
+++ b/README-zh_CN.md
@@ -90,22 +90,22 @@ OpenIM 我们的目标是建立一个顶级的开源社区。我们有一套标
在开始之前,请确保你的更改是有需求的。最好的方法是创建一个[新的讨论](https://github.com/openimsdk/open-im-server/discussions/new/choose) 或 [Slack 通信](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q),或者如果你发现一个问题,首先[报告它](https://github.com/openimsdk/open-im-server/issues/new/choose)。
-+ [代码标准](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/go_code.md)
++ [代码标准](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/go-code.md)
-+ [Docker 镜像标准](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/images.md)
++ [Docker 镜像标准](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/images.md)
-+ [目录标准](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/directory.md)
++ [目录标准](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/directory.md)
-+ [提交标准](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/commit.md)
++ [提交标准](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/commit.md)
-+ [版本控制标准](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/version.md)
++ [版本控制标准](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/version.md)
-+ [接口标准](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/interface.md)
++ [接口标准](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/interface.md)
+ [OpenIM配置和环境变量设置](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/environment.md)
> **Note**
-> 针对中国的用户,阅读我们的 [Docker 镜像标准](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/images.md) 以便使用国内 aliyun 的镜像地址。OpenIM 也有针对中国的 gitee 同步仓库,你可以在 [gitee.com](https://gitee.com/openimsdk) 上找到它。
+> 针对中国的用户,阅读我们的 [Docker 镜像标准](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/images.md) 以便使用国内 aliyun 的镜像地址。OpenIM 也有针对中国的 gitee 同步仓库,你可以在 [gitee.com](https://gitee.com/openimsdk) 上找到它。
## :link: 链接
diff --git a/README.md b/README.md
index 0a569af6e..722de0240 100644
--- a/README.md
+++ b/README.md
@@ -4,31 +4,28 @@
-
- ⭐️ Open source Instant Messaging Server ⭐️
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+[](https://github.com/openimsdk/open-im-server/stargazers)
+[](https://github.com/openimsdk/open-im-server/network/members)
+[](https://app.codecov.io/gh/openimsdk/open-im-server)
+[](https://goreportcard.com/report/github.com/openimsdk/open-im-server)
+[](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)
+[](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)
+[](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q)
+[](https://www.bestpractices.dev/projects/8045)
+[](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)
+[](https://golang.org/)
+
+[**English**](./README.md) •
+[**简体中文**](./README-zh_CN.md) •
+[**Docs**](https://openim.io/en)
-
- English •
- 简体中文 •
- Docs
-
+
+
## Ⓜ️ About OpenIM
OpenIM isn't just an open-source instant messaging component, it's an integral part of your application ecosystem. Check out this diagram to understand how AppServer, AppClient, OpenIMServer, and OpenIMSDK interact.
@@ -118,7 +115,7 @@ It is recommended to use Docker Compose for deployment, which can easily and qui
> **Note**
>
-> If you don't know OpenIM's versioning policy, 📚Read our release policy: https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/version.md
+> If you don't know OpenIM's versioning policy, 📚Read our release policy: https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/version.md
@@ -133,7 +130,7 @@ Ur need `Go 1.20` or higher version, and `make`.
go version && make --version || echo "Error: One of the commands failed."
```
-Version Details: https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/version.md
+Version Details: https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/version.md
You can get the version number from the command below or from [github releases](https://github.com/openimsdk/open-im-server/tags).
@@ -141,7 +138,7 @@ You can get the version number from the command below or from [github releases](
$ curl --silent "https://api.github.com/repos/openimsdk/open-im-server/releases" | jq -r '.[].tag_name'
```
-We have our own version management policy, if you are interested in our version management, I recommend reading [📚 OpenIM Version](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/version.md), We recommend using stable versions such as `v3.3.0` and `v3.2.0` whenever possible. `v3.1.1-alpha.3` as well as `v3.3.0-beta.0` and `v3.2.0-rc.0` are pre-release or beta versions and are not recommended.
+We have our own version management policy, if you are interested in our version management, I recommend reading [📚 OpenIM Version](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/version.md), We recommend using stable versions such as `v3.3.0` and `v3.2.0` whenever possible. `v3.1.1-alpha.3` as well as `v3.3.0-beta.0` and `v3.2.0-rc.0` are pre-release or beta versions and are not recommended.
Set `OPENIM_VERSION` environment variables for the latest `OPENIM_VERSION` number, or replace the `OPENIM_VERSION` for you to install the OpenIM-Server `OPENIM_VERSION`:
@@ -199,21 +196,41 @@ Delve into the heart of Open-IM-Server's functionality with our architecture dia
## :hammer_and_wrench: To start developing OpenIM
+[](https://vscode.dev/github/openimsdk/open-im-server)
+
OpenIM Our goal is to build a top-level open source community. We have a set of standards, in the [Community repository](https://github.com/OpenIMSDK/community).
If you'd like to contribute to this Open-IM-Server repository, please read our [contributor documentation](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md).
Before you start, please make sure your changes are in demand. The best for that is to create a [new discussion](https://github.com/openimsdk/open-im-server/discussions/new/choose) OR [Slack Communication](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q), or if you find an issue, [report it](https://github.com/openimsdk/open-im-server/issues/new/choose) first.
-- [Code Standards](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/go_code.md)
-- [Docker Images Standards](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/images.md)
-- [Directory Standards](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/directory.md)
-- [Commit Standards](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/commit.md)
-- [Versioning Standards](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/version.md)
-- [Interface Standards](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/api.md)
-- [Log Standards](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/logging.md)
-- [Error Code Standards](https://github.com/openimsdk/open-im-server/blob/main/docs/conversions/error_code.md)
-- [OpenIM configuration and environment variable Settings](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/environment.md)
+- [OpenIM API Reference](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md)
+- [OpenIM Bash Logging](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md)
+- [OpenIM CI/CD Actions](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md)
+- [OpenIM Code Conventions](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md)
+- [OpenIM Commit Guidelines](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md)
+- [OpenIM Development Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md)
+- [OpenIM Directory Structure](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md)
+- [OpenIM Environment Setup](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md)
+- [OpenIM Error Code Reference](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md)
+- [OpenIM Git Workflow](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md)
+- [OpenIM Git Cherry Pick Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/gitcherry-pick.md)
+- [OpenIM GitHub Workflow](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md)
+- [OpenIM Go Code Standards](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md)
+- [OpenIM Image Guidelines](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md)
+- [OpenIM Initial Configuration](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/init-config.md)
+- [OpenIM Docker Installation Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md)
+- [OpenIM OpenIM Linux System Installation](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md)
+- [OpenIM Linux Development Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/linux-development.md)
+- [OpenIM Local Actions Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md)
+- [OpenIM Logging Conventions](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md)
+- [OpenIM Offline Deployment](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md)
+- [OpenIM Protoc Tools](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md)
+- [OpenIM Testing Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md)
+- [OpenIM Utility Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md)
+- [OpenIM Makefile Utilities](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md)
+- [OpenIM Script Utilities](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md)
+- [OpenIM Versioning](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md)
## :busts_in_silhouette: Community
diff --git a/build/images/Dockerfile b/build/images/Dockerfile
index 518de78b1..51fe94e1c 100644
--- a/build/images/Dockerfile
+++ b/build/images/Dockerfile
@@ -1,3 +1,17 @@
+# Copyright © 2023 OpenIM. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
FROM BASE_IMAGE
WORKDIR ${SERVER_WORKDIR}
diff --git a/chat b/chat
new file mode 100755
index 0000000000000000000000000000000000000000..b79e06dd7fc98898f96d053443784f8a4e760d9b
GIT binary patch
literal 7068773
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
z
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
@DI=u^R(r!Zg*Ql!YX2B992D|d
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