commit
c7b6b0f3ad
@ -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)
|
@ -0,0 +1,42 @@
|
|||||||
|
# Contrib Documentation Index
|
||||||
|
|
||||||
|
## 📚 General Information
|
||||||
|
- [📄 README](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/README.md) - General introduction to the contribution documentation.
|
||||||
|
- [📑 Development Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md) - Guidelines for setting up a development environment.
|
||||||
|
|
||||||
|
## 🛠 Setup and Installation
|
||||||
|
- [🌍 Environment Setup](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md) - Instructions on setting up the development environment.
|
||||||
|
- [🐳 Docker Installation Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md) - Steps to install Docker for container management.
|
||||||
|
- [🔧 OpenIM Linux System Installation](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md) - Guide for installing OpenIM on a Linux system.
|
||||||
|
|
||||||
|
## 💻 Development Practices
|
||||||
|
- [👨💻 Code Conventions](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md) - Coding standards to follow for consistency.
|
||||||
|
- [📐 Directory Structure](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md) - Explanation of the repository's directory layout.
|
||||||
|
- [🔀 Git Workflow](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md) - The workflow for using Git in this project (note the file extension error).
|
||||||
|
- [💾 GitHub Workflow](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md) - Workflow guidelines for GitHub.
|
||||||
|
|
||||||
|
## 🧪 Testing and Deployment
|
||||||
|
- [⚙️ CI/CD Actions](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md) - Continuous integration and deployment configurations.
|
||||||
|
- [🚀 Offline Deployment](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md) - How to deploy the application offline.
|
||||||
|
|
||||||
|
## 🔧 Utilities and Tools
|
||||||
|
- [📦 Protoc Tools](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md) - Protobuf compiler-related utilities.
|
||||||
|
- [🔨 Utility Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md) - Go utilities and helper functions.
|
||||||
|
- [🛠 Makefile Utilities](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md) - Makefile scripts for automation.
|
||||||
|
- [📜 Script Utilities](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md) - Utility scripts for development.
|
||||||
|
|
||||||
|
## 📋 Standards and Conventions
|
||||||
|
- [🚦 Commit Guidelines](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md) - Standards for writing commit messages.
|
||||||
|
- [✅ Testing Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md) - Guidelines and conventions for writing tests.
|
||||||
|
- [📈 Versioning](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md) - Version management for the project.
|
||||||
|
|
||||||
|
## 🖼 Additional Resources
|
||||||
|
- [🌐 API Reference](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) - Detailed API documentation.
|
||||||
|
- [📚 Go Code Standards](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md) - Go programming language standards.
|
||||||
|
- [🖼 Image Guidelines](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md) - Guidelines for image assets.
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
- [🔍 Error Code Reference](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md) - List of error codes and their meanings.
|
||||||
|
- [🐚 Bash Logging](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md) - Logging standards for bash scripts.
|
||||||
|
- [📈 Logging Conventions](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md) - Conventions for application logging.
|
||||||
|
- [🛠 Local Actions Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md) - How to perform local actions for troubleshooting.
|
@ -1,10 +0,0 @@
|
|||||||
## OpenIM Project Development Standards
|
|
||||||
|
|
||||||
- [Code Standards](./go_code.md)
|
|
||||||
- [Docker Images Standards](./images.md)
|
|
||||||
- [Directory Standards](./directory.md)
|
|
||||||
- [Commit Standards](./commit.md)
|
|
||||||
- [Versioning Standards](./version.md)
|
|
||||||
- [Interface Standards](./api.md)
|
|
||||||
- [Log Standards](./log.md)
|
|
||||||
- [Error Code Standards](./error_code.md)
|
|
@ -0,0 +1,107 @@
|
|||||||
|
package msggateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mockRandom() []byte {
|
||||||
|
bs := make([]byte, 50)
|
||||||
|
rand.Read(bs)
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompressDecompress(t *testing.T) {
|
||||||
|
|
||||||
|
compressor := NewGzipCompressor()
|
||||||
|
|
||||||
|
for i := 0; i < 2000; i++ {
|
||||||
|
src := mockRandom()
|
||||||
|
|
||||||
|
// compress
|
||||||
|
dest, err := compressor.CompressWithPool(src)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
|
||||||
|
// decompress
|
||||||
|
res, err := compressor.DecompressWithPool(dest)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
|
||||||
|
// check
|
||||||
|
assert.EqualValues(t, src, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompressDecompressWithConcurrency(t *testing.T) {
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
compressor := NewGzipCompressor()
|
||||||
|
|
||||||
|
for i := 0; i < 200; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
src := mockRandom()
|
||||||
|
|
||||||
|
// compress
|
||||||
|
dest, err := compressor.CompressWithPool(src)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
|
||||||
|
// decompress
|
||||||
|
res, err := compressor.DecompressWithPool(dest)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
|
||||||
|
// check
|
||||||
|
assert.EqualValues(t, src, res)
|
||||||
|
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkCompress(b *testing.B) {
|
||||||
|
src := mockRandom()
|
||||||
|
compressor := NewGzipCompressor()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := compressor.Compress(src)
|
||||||
|
assert.Equal(b, nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkCompressWithSyncPool(b *testing.B) {
|
||||||
|
src := mockRandom()
|
||||||
|
|
||||||
|
compressor := NewGzipCompressor()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := compressor.CompressWithPool(src)
|
||||||
|
assert.Equal(b, nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecompress(b *testing.B) {
|
||||||
|
src := mockRandom()
|
||||||
|
|
||||||
|
compressor := NewGzipCompressor()
|
||||||
|
comdata, err := compressor.Compress(src)
|
||||||
|
assert.Equal(b, nil, err)
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := compressor.DeCompress(comdata)
|
||||||
|
assert.Equal(b, nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecompressWithSyncPool(b *testing.B) {
|
||||||
|
src := mockRandom()
|
||||||
|
|
||||||
|
compressor := NewGzipCompressor()
|
||||||
|
comdata, err := compressor.Compress(src)
|
||||||
|
assert.Equal(b, nil, err)
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := compressor.DecompressWithPool(comdata)
|
||||||
|
assert.Equal(b, nil, err)
|
||||||
|
}
|
||||||
|
}
|
@ -1,32 +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 fcm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush"
|
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/db/cache"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Push(t *testing.T) {
|
|
||||||
var redis cache.MsgModel
|
|
||||||
offlinePusher := NewClient(redis)
|
|
||||||
err := offlinePusher.Push(context.Background(), []string{"userID1"}, "test", "test", &offlinepush.Opts{})
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
@ -0,0 +1,51 @@
|
|||||||
|
// 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/OpenIMSDK/protocol/constant"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"gotest.tools/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockRootCmd is a mock type for the RootCmd type
|
||||||
|
type MockRootCmd struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockRootCmd) Execute() error {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgGatewayCmd_GetPortFromConfig(t *testing.T) {
|
||||||
|
msgGatewayCmd := &MsgGatewayCmd{RootCmd: &RootCmd{}}
|
||||||
|
tests := []struct {
|
||||||
|
portType string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{constant.FlagWsPort, 8080}, // Replace 8080 with the expected port from the config
|
||||||
|
{constant.FlagPort, 8081}, // Replace 8081 with the expected port from the config
|
||||||
|
{"invalid", 0},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.portType, func(t *testing.T) {
|
||||||
|
got := msgGatewayCmd.GetPortFromConfig(tt.portType)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
// 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 config
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/msgprocessor"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetDefaultConfigPath(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := GetDefaultConfigPath(); got != tt.want {
|
||||||
|
t.Errorf("GetDefaultConfigPath() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetProjectRoot(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := GetProjectRoot(); got != tt.want {
|
||||||
|
t.Errorf("GetProjectRoot() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOptionsByNotification(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
cfg NotificationConf
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want msgprocessor.Options
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := GetOptionsByNotification(tt.args.cfg); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("GetOptionsByNotification() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_initConfig(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
config interface{}
|
||||||
|
configName string
|
||||||
|
configFolderPath string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := initConfig(tt.args.config, tt.args.configName, tt.args.configFolderPath); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("initConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitConfig(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
configFolderPath string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := InitConfig(tt.args.configFolderPath); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("InitConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package discovery_register
|
package discoveryregister
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@ -0,0 +1,407 @@
|
|||||||
|
package discoveryregister
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/OpenIMSDK/tools/discoveryregistry"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewDiscoveryRegister(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
envType string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want discoveryregistry.SvcDiscoveryRegistry
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := NewDiscoveryRegister(tt.args.envType)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("NewDiscoveryRegister() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("NewDiscoveryRegister() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewK8sDiscoveryRegister(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want discoveryregistry.SvcDiscoveryRegistry
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := NewK8sDiscoveryRegister()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("NewK8sDiscoveryRegister() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("NewK8sDiscoveryRegister() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sDR_Register(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
options []grpc.DialOption
|
||||||
|
rpcRegisterAddr string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
serviceName string
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
opts []grpc.DialOption
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cli := &K8sDR{
|
||||||
|
options: tt.fields.options,
|
||||||
|
rpcRegisterAddr: tt.fields.rpcRegisterAddr,
|
||||||
|
}
|
||||||
|
if err := cli.Register(tt.args.serviceName, tt.args.host, tt.args.port, tt.args.opts...); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("K8sDR.Register() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sDR_UnRegister(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
options []grpc.DialOption
|
||||||
|
rpcRegisterAddr string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cli := &K8sDR{
|
||||||
|
options: tt.fields.options,
|
||||||
|
rpcRegisterAddr: tt.fields.rpcRegisterAddr,
|
||||||
|
}
|
||||||
|
if err := cli.UnRegister(); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("K8sDR.UnRegister() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sDR_CreateRpcRootNodes(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
options []grpc.DialOption
|
||||||
|
rpcRegisterAddr string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
serviceNames []string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cli := &K8sDR{
|
||||||
|
options: tt.fields.options,
|
||||||
|
rpcRegisterAddr: tt.fields.rpcRegisterAddr,
|
||||||
|
}
|
||||||
|
if err := cli.CreateRpcRootNodes(tt.args.serviceNames); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("K8sDR.CreateRpcRootNodes() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sDR_RegisterConf2Registry(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
options []grpc.DialOption
|
||||||
|
rpcRegisterAddr string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
key string
|
||||||
|
conf []byte
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cli := &K8sDR{
|
||||||
|
options: tt.fields.options,
|
||||||
|
rpcRegisterAddr: tt.fields.rpcRegisterAddr,
|
||||||
|
}
|
||||||
|
if err := cli.RegisterConf2Registry(tt.args.key, tt.args.conf); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("K8sDR.RegisterConf2Registry() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sDR_GetConfFromRegistry(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
options []grpc.DialOption
|
||||||
|
rpcRegisterAddr string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want []byte
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cli := &K8sDR{
|
||||||
|
options: tt.fields.options,
|
||||||
|
rpcRegisterAddr: tt.fields.rpcRegisterAddr,
|
||||||
|
}
|
||||||
|
got, err := cli.GetConfFromRegistry(tt.args.key)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("K8sDR.GetConfFromRegistry() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("K8sDR.GetConfFromRegistry() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sDR_GetConns(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
options []grpc.DialOption
|
||||||
|
rpcRegisterAddr string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
serviceName string
|
||||||
|
opts []grpc.DialOption
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want []*grpc.ClientConn
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cli := &K8sDR{
|
||||||
|
options: tt.fields.options,
|
||||||
|
rpcRegisterAddr: tt.fields.rpcRegisterAddr,
|
||||||
|
}
|
||||||
|
got, err := cli.GetConns(tt.args.ctx, tt.args.serviceName, tt.args.opts...)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("K8sDR.GetConns() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("K8sDR.GetConns() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sDR_GetConn(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
options []grpc.DialOption
|
||||||
|
rpcRegisterAddr string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
serviceName string
|
||||||
|
opts []grpc.DialOption
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want *grpc.ClientConn
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cli := &K8sDR{
|
||||||
|
options: tt.fields.options,
|
||||||
|
rpcRegisterAddr: tt.fields.rpcRegisterAddr,
|
||||||
|
}
|
||||||
|
got, err := cli.GetConn(tt.args.ctx, tt.args.serviceName, tt.args.opts...)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("K8sDR.GetConn() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("K8sDR.GetConn() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sDR_GetSelfConnTarget(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
options []grpc.DialOption
|
||||||
|
rpcRegisterAddr string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cli := &K8sDR{
|
||||||
|
options: tt.fields.options,
|
||||||
|
rpcRegisterAddr: tt.fields.rpcRegisterAddr,
|
||||||
|
}
|
||||||
|
if got := cli.GetSelfConnTarget(); got != tt.want {
|
||||||
|
t.Errorf("K8sDR.GetSelfConnTarget() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sDR_AddOption(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
options []grpc.DialOption
|
||||||
|
rpcRegisterAddr string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
opts []grpc.DialOption
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cli := &K8sDR{
|
||||||
|
options: tt.fields.options,
|
||||||
|
rpcRegisterAddr: tt.fields.rpcRegisterAddr,
|
||||||
|
}
|
||||||
|
cli.AddOption(tt.args.opts...)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sDR_CloseConn(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
options []grpc.DialOption
|
||||||
|
rpcRegisterAddr string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cli := &K8sDR{
|
||||||
|
options: tt.fields.options,
|
||||||
|
rpcRegisterAddr: tt.fields.rpcRegisterAddr,
|
||||||
|
}
|
||||||
|
cli.CloseConn(tt.args.conn)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sDR_GetClientLocalConns(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
options []grpc.DialOption
|
||||||
|
rpcRegisterAddr string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want map[string][]*grpc.ClientConn
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cli := &K8sDR{
|
||||||
|
options: tt.fields.options,
|
||||||
|
rpcRegisterAddr: tt.fields.rpcRegisterAddr,
|
||||||
|
}
|
||||||
|
if got := cli.GetClientLocalConns(); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("K8sDR.GetClientLocalConns() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sDR_Close(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
options []grpc.DialOption
|
||||||
|
rpcRegisterAddr string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cli := &K8sDR{
|
||||||
|
options: tt.fields.options,
|
||||||
|
rpcRegisterAddr: tt.fields.rpcRegisterAddr,
|
||||||
|
}
|
||||||
|
cli.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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, <optional []*Metric> ).")
|
||||||
|
} 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
|
||||||
|
}
|
@ -0,0 +1,154 @@
|
|||||||
|
// 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 http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/callbackstruct"
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantResponse []byte
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotResponse, err := Get(tt.args.url)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(gotResponse, tt.wantResponse) {
|
||||||
|
t.Errorf("Get() = %v, want %v", gotResponse, tt.wantResponse)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPost(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
url string
|
||||||
|
header map[string]string
|
||||||
|
data interface{}
|
||||||
|
timeout int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantContent []byte
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotContent, err := Post(tt.args.ctx, tt.args.url, tt.args.header, tt.args.data, tt.args.timeout)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Post() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(gotContent, tt.wantContent) {
|
||||||
|
t.Errorf("Post() = %v, want %v", gotContent, tt.wantContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostReturn(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
url string
|
||||||
|
header map[string]string
|
||||||
|
input interface{}
|
||||||
|
output interface{}
|
||||||
|
timeOutSecond int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := PostReturn(tt.args.ctx, tt.args.url, tt.args.header, tt.args.input, tt.args.output, tt.args.timeOutSecond); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("PostReturn() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_callBackPostReturn(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
url string
|
||||||
|
command string
|
||||||
|
input interface{}
|
||||||
|
output callbackstruct.CallbackResp
|
||||||
|
callbackConfig config.CallBackConfig
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := callBackPostReturn(tt.args.ctx, tt.args.url, tt.args.command, tt.args.input, tt.args.output, tt.args.callbackConfig); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("callBackPostReturn() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallBackPostReturn(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
url string
|
||||||
|
req callbackstruct.CallbackReq
|
||||||
|
resp callbackstruct.CallbackResp
|
||||||
|
callbackConfig config.CallBackConfig
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := CallBackPostReturn(tt.args.ctx, tt.args.url, tt.args.req, tt.args.resp, tt.args.callbackConfig); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("CallBackPostReturn() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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 locker // import "github.com/openimsdk/open-im-server/v3/pkg/common/locker"
|
|
@ -1,72 +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 locker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/openimsdk/open-im-server/v3/pkg/common/db/cache"
|
|
||||||
)
|
|
||||||
|
|
||||||
const GlOBALLOCK = "GLOBAL_LOCK"
|
|
||||||
|
|
||||||
type MessageLocker interface {
|
|
||||||
LockMessageTypeKey(ctx context.Context, clientMsgID, typeKey string) (err error)
|
|
||||||
UnLockMessageTypeKey(ctx context.Context, clientMsgID string, typeKey string) error
|
|
||||||
LockGlobalMessage(ctx context.Context, clientMsgID string) (err error)
|
|
||||||
UnLockGlobalMessage(ctx context.Context, clientMsgID string) (err error)
|
|
||||||
}
|
|
||||||
type LockerMessage struct {
|
|
||||||
cache cache.MsgModel
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLockerMessage(cache cache.MsgModel) *LockerMessage {
|
|
||||||
return &LockerMessage{cache: cache}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LockerMessage) LockMessageTypeKey(ctx context.Context, clientMsgID, typeKey string) (err error) {
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
err = l.cache.LockMessageTypeKey(ctx, clientMsgID, typeKey)
|
|
||||||
if err != nil {
|
|
||||||
time.Sleep(time.Millisecond * 100)
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LockerMessage) LockGlobalMessage(ctx context.Context, clientMsgID string) (err error) {
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
err = l.cache.LockMessageTypeKey(ctx, clientMsgID, GlOBALLOCK)
|
|
||||||
if err != nil {
|
|
||||||
time.Sleep(time.Millisecond * 100)
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LockerMessage) UnLockMessageTypeKey(ctx context.Context, clientMsgID string, typeKey string) error {
|
|
||||||
return l.cache.UnLockMessageTypeKey(ctx, clientMsgID, typeKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LockerMessage) UnLockGlobalMessage(ctx context.Context, clientMsgID string) error {
|
|
||||||
return l.cache.UnLockMessageTypeKey(ctx, clientMsgID, GlOBALLOCK)
|
|
||||||
}
|
|
@ -0,0 +1,16 @@
|
|||||||
|
package prommetrics
|
||||||
|
|
||||||
|
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"},
|
||||||
|
}
|
||||||
|
)
|
@ -0,0 +1,12 @@
|
|||||||
|
package prommetrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
UserLoginCounter = prometheus.NewCounter(prometheus.CounterOpts{
|
||||||
|
Name: "user_login_total",
|
||||||
|
Help: "The number of user login",
|
||||||
|
})
|
||||||
|
)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue