From 279021bf86f3d593035d089b674b6fab2841eb98 Mon Sep 17 00:00:00 2001 From: zxr <271055687@qq.com> Date: Mon, 30 Mar 2026 15:26:16 +0800 Subject: [PATCH] fix --- .gitignore | 47 +++ cmd/main/main.go | 35 +++ etc/logs_dev.yaml | 26 ++ etc/supervisor.ops-logs.conf | 18 ++ go.mod | 76 +++++ go.sum | 227 ++++++++++++++ internal/config/config.go | 43 +++ internal/impl/impl.go | 32 ++ internal/ingest/alert_forward.go | 64 ++++ internal/ingest/engine.go | 426 +++++++++++++++++++++++++++ internal/ingest/shield.go | 114 +++++++ internal/ingest/syslog_parse.go | 109 +++++++ internal/ingest/syslog_parse_test.go | 32 ++ internal/ingest/syslog_udp.go | 41 +++ internal/ingest/trap_udp.go | 32 ++ internal/logic/controllers/crud.go | 297 +++++++++++++++++++ internal/logic/ping/ping.go | 10 + internal/models/log_event.go | 21 ++ internal/models/query.go | 17 ++ internal/models/syslog_rule.go | 21 ++ internal/models/trap_dictionary.go | 19 ++ internal/models/trap_rule.go | 21 ++ internal/models/trap_shield.go | 19 ++ internal/routers/register.go | 44 +++ scripts/pack.ps1 | 3 + 25 files changed, 1794 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/main/main.go create mode 100644 etc/logs_dev.yaml create mode 100644 etc/supervisor.ops-logs.conf create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/config/config.go create mode 100644 internal/impl/impl.go create mode 100644 internal/ingest/alert_forward.go create mode 100644 internal/ingest/engine.go create mode 100644 internal/ingest/shield.go create mode 100644 internal/ingest/syslog_parse.go create mode 100644 internal/ingest/syslog_parse_test.go create mode 100644 internal/ingest/syslog_udp.go create mode 100644 internal/ingest/trap_udp.go create mode 100644 internal/logic/controllers/crud.go create mode 100644 internal/logic/ping/ping.go create mode 100644 internal/models/log_event.go create mode 100644 internal/models/query.go create mode 100644 internal/models/syslog_rule.go create mode 100644 internal/models/trap_dictionary.go create mode 100644 internal/models/trap_rule.go create mode 100644 internal/models/trap_shield.go create mode 100644 internal/routers/register.go create mode 100644 scripts/pack.ps1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65cac43 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Dependency directories +vendor/ + +# Go workspace file +go.work + +# IDEs +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Build artifacts +ops-logs +dist/ +build/ + +# Logs +*.log +logs/ + +# Config files with sensitive data +*_local.yaml +.env + +# Temporary files +tmp/ +temp/ + diff --git a/cmd/main/main.go b/cmd/main/main.go new file mode 100644 index 0000000..fe50c61 --- /dev/null +++ b/cmd/main/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + + "git.apinb.com/bsm-sdk/core/infra" + "git.apinb.com/bsm-sdk/core/middleware" + "git.apinb.com/ops/logs/internal/config" + "git.apinb.com/ops/logs/internal/impl" + "git.apinb.com/ops/logs/internal/ingest" + "git.apinb.com/ops/logs/internal/routers" + "github.com/gin-gonic/gin" +) + +var ServiceKey = "Logs" + +func main() { + config.New(ServiceKey) + impl.NewImpl() + + ingest.StartRefresher() + ingest.StartSyslogUDP() + ingest.StartTrapUDP() + + app := gin.Default() + middleware.Mode(app) + app.Use(middleware.Cors()) + app.Use(gin.Recovery()) + app.HEAD("/", infra.Health) + routers.Register(ServiceKey, app) + + if err := app.Run(fmt.Sprintf(":%s", config.Spec.Port)); err != nil { + panic(err) + } +} diff --git a/etc/logs_dev.yaml b/etc/logs_dev.yaml new file mode 100644 index 0000000..bb5aadf --- /dev/null +++ b/etc/logs_dev.yaml @@ -0,0 +1,26 @@ +Service: logs +Port: 12440 + +Databases: + Driver: postgres + Source: + - host=8.137.107.29 user=postgres password=Weidong2023~! dbname=ops_dev port=19432 sslmode=disable TimeZone=Asia/Shanghai + +# cache DB的选择请在后面直接带参数,不带会自动HASH计算选择DB库。 +Cache: redis://null:Weidong2023~!@8.137.107.29:19379/ + +MicroService: + Enable: false + Anonymous: + - ops.ping.hello + +Ingest: + syslog_listen_addr: "0.0.0.0:5514" + trap_listen_addr: "0.0.0.0:9162" + rule_refresh_secs: 30 + +AlertForward: + enabled: true + base_url: https://ops2.apinb.com + internal_key: "ops-alert" + default_policy_id: 0 diff --git a/etc/supervisor.ops-logs.conf b/etc/supervisor.ops-logs.conf new file mode 100644 index 0000000..debed05 --- /dev/null +++ b/etc/supervisor.ops-logs.conf @@ -0,0 +1,18 @@ +[program:ops-logs] +command=/data/app/ops-logs +directory=/data/app +autostart=true +autorestart=true +user=root +redirect_stderr=true +stdout_logfile=/data/app/logs/ops-logs.log + + + + + + + + + + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..658df03 --- /dev/null +++ b/go.mod @@ -0,0 +1,76 @@ +module git.apinb.com/ops/logs + +go 1.25.1 + +require ( + git.apinb.com/bsm-sdk/core v0.1.3 + github.com/gin-gonic/gin v1.12.0 + github.com/gosnmp/gosnmp v1.37.0 + gorm.io/gorm v1.31.0 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/allegro/bigcache/v3 v3.1.0 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/cors v1.7.7 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/redis/go-redis/v9 v9.18.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.etcd.io/etcd/api/v3 v3.6.9 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.9 // indirect + go.etcd.io/etcd/client/v3 v3.6.9 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/arch v0.23.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/mysql v1.6.0 // indirect + gorm.io/driver/postgres v1.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..79e35f1 --- /dev/null +++ b/go.sum @@ -0,0 +1,227 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +git.apinb.com/bsm-sdk/core v0.1.3 h1:Kun3YS6lg0lvzjbyi61V3SFJMElyVuDruONDO1UErmk= +git.apinb.com/bsm-sdk/core v0.1.3/go.mod h1:rCmMma8R2pvByImgoZDm2OPLdr+IUNr7LBPyayb8aN0= +github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk= +github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/cors v1.7.7 h1:Oh9joP463x7Mw72vhvJ61YQm8ODh9b04YR7vsOErD0Q= +github.com/gin-contrib/cors v1.7.7/go.mod h1:K5tW0RkzJtWSiOdikXloy8VEZlgdVNpHNw8FpjUPNrE= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gosnmp/gosnmp v1.37.0 h1:/Tf8D3b9wrnNuf/SfbvO+44mPrjVphBhRtcGg22V07Y= +github.com/gosnmp/gosnmp v1.37.0/go.mod h1:GDH9vNqpsD7f2HvZhKs5dlqSEcAS6s6Qp099oZRCR+M= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.etcd.io/etcd/api/v3 v3.6.9 h1:UA7iKfEW1AzgihcBSGXci2kDGQiokSq41F9HMCI/RTI= +go.etcd.io/etcd/api/v3 v3.6.9/go.mod h1:csEk/qTfxKL36NqJdU15Tgtl65A8dyEY2BYo7PRsIwk= +go.etcd.io/etcd/client/pkg/v3 v3.6.9 h1:T8nuk8Lz64C+Hzb0coBFLMSlVSQZBpAtFk46swdM1DA= +go.etcd.io/etcd/client/pkg/v3 v3.6.9/go.mod h1:WEy3PpwbbEBVRdh1NVJYsuUe/8eyI21PNJRazeD8z/Y= +go.etcd.io/etcd/client/v3 v3.6.9 h1:3X555hQXmhRr27O37wls53g68CpUiPOiHXrZfz2Al+o= +go.etcd.io/etcd/client/v3 v3.6.9/go.mod h1:KO7H1HLYh1qaljuVZJQwBFk1lRce6pJzt+C81GEnrlM= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= +golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..4992ffa --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,43 @@ +package config + +import ( + "net" + + "git.apinb.com/bsm-sdk/core/conf" +) + +var Spec SrvConfig + +type AlertForwardConf struct { + BaseURL string `yaml:"base_url"` + InternalKey string `yaml:"internal_key"` + Enabled bool `yaml:"enabled"` + DefaultPolicyID uint `yaml:"default_policy_id"` +} + +type IngestConf struct { + SyslogListenAddr string `yaml:"syslog_listen_addr"` + TrapListenAddr string `yaml:"trap_listen_addr"` + RuleRefreshSecs int `yaml:"rule_refresh_secs"` +} + +type SrvConfig struct { + conf.Base `yaml:",inline"` + Databases *conf.DBConf `yaml:"Databases"` + MicroService *conf.MicroServiceConf `yaml:"MicroService"` + Rpc map[string]conf.RpcConf `yaml:"Rpc"` + Gateway *conf.GatewayConf `yaml:"Gateway"` + Apm *conf.ApmConf `yaml:"APM"` + Etcd *conf.EtcdConf `yaml:"Etcd"` + AlertForward *AlertForwardConf `yaml:"AlertForward"` + Ingest IngestConf `yaml:"Ingest"` +} + +func New(srvKey string) { + conf.New(srvKey, &Spec) + Spec.Port = conf.CheckPort(Spec.Port) + Spec.BindIP = conf.CheckIP(Spec.BindIP) + Spec.Addr = net.JoinHostPort(Spec.BindIP, Spec.Port) + conf.NotNil(Spec.Service, Spec.Cache) + conf.PrintInfo(Spec.Addr) +} diff --git a/internal/impl/impl.go b/internal/impl/impl.go new file mode 100644 index 0000000..93ed4bf --- /dev/null +++ b/internal/impl/impl.go @@ -0,0 +1,32 @@ +package impl + +import ( + "fmt" + + "git.apinb.com/bsm-sdk/core/cache/redis" + "git.apinb.com/bsm-sdk/core/logger" + "git.apinb.com/bsm-sdk/core/with" + "git.apinb.com/ops/logs/internal/config" + "git.apinb.com/ops/logs/internal/models" + "gorm.io/gorm" +) + +var ( + RedisService *redis.RedisClient + DBService *gorm.DB +) + +func NewImpl() { + RedisService = with.RedisCache(config.Spec.Cache) + DBService = with.Databases(config.Spec.Databases, nil) + logger.New(nil) + + if DBService != nil { + if err := DBService.AutoMigrate(models.GetAllModels()...); err != nil { + panic(fmt.Sprintf("logs migrate: %v", err)) + } + if err := models.InitData(); err != nil { + panic(fmt.Sprintf("logs init data: %v", err)) + } + } +} diff --git a/internal/ingest/alert_forward.go b/internal/ingest/alert_forward.go new file mode 100644 index 0000000..a420446 --- /dev/null +++ b/internal/ingest/alert_forward.go @@ -0,0 +1,64 @@ +package ingest + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" + + "git.apinb.com/ops/logs/internal/config" +) + +// AlertReceiveBody 与 alert ReceiveRequest 对齐(含必填 raw_data) +type AlertReceiveBody struct { + AlertName string `json:"alert_name"` + Summary string `json:"summary"` + Description string `json:"description"` + SeverityCode string `json:"severity_code"` + Value string `json:"value"` + Threshold string `json:"threshold"` + Labels map[string]string `json:"labels"` + Agent string `json:"agent"` + PolicyID uint `json:"policy_id"` + RawData json.RawMessage `json:"raw_data"` +} + +func forwardAlert(body AlertReceiveBody) error { + cfg := config.Spec.AlertForward + if cfg == nil || !cfg.Enabled || cfg.BaseURL == "" { + return nil + } + if len(body.RawData) == 0 { + return fmt.Errorf("raw_data 不能为空") + } + if body.AlertName == "" { + body.AlertName = "日志告警" + } + if body.PolicyID == 0 && cfg.DefaultPolicyID > 0 { + body.PolicyID = cfg.DefaultPolicyID + } + raw, err := json.Marshal(body) + if err != nil { + return err + } + url := cfg.BaseURL + "/Alert/v1/alerts/receive" + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(raw)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + if cfg.InternalKey != "" { + req.Header.Set("X-Internal-Key", cfg.InternalKey) + } + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("alert returned HTTP %d", resp.StatusCode) + } + return nil +} diff --git a/internal/ingest/engine.go b/internal/ingest/engine.go new file mode 100644 index 0000000..fc82bc1 --- /dev/null +++ b/internal/ingest/engine.go @@ -0,0 +1,426 @@ +package ingest + +import ( + "encoding/json" + "fmt" + "net" + "regexp" + "sort" + "strconv" + "strings" + "sync" + "time" + + "git.apinb.com/ops/logs/internal/config" + "git.apinb.com/ops/logs/internal/impl" + "git.apinb.com/ops/logs/internal/models" + "github.com/gosnmp/gosnmp" +) + +type Engine struct { + mu sync.RWMutex + + trapDict []models.TrapDictionaryEntry + syslogRules []models.SyslogRule + trapRules []models.TrapRule + shields []models.TrapShield +} + +var Global = &Engine{} + +func (e *Engine) Refresh() error { + var dict []models.TrapDictionaryEntry + var syslog []models.SyslogRule + var trap []models.TrapRule + var shield []models.TrapShield + + if err := impl.DBService.Where("enabled = ?", true).Find(&dict).Error; err != nil { + return err + } + sort.Slice(dict, func(i, j int) bool { + return len(dict[i].OIDPrefix) > len(dict[j].OIDPrefix) + }) + + if err := impl.DBService.Where("enabled = ?", true).Find(&syslog).Error; err != nil { + return err + } + sort.Slice(syslog, func(i, j int) bool { return syslog[i].Priority > syslog[j].Priority }) + + if err := impl.DBService.Where("enabled = ?", true).Find(&trap).Error; err != nil { + return err + } + sort.Slice(trap, func(i, j int) bool { return trap[i].Priority > trap[j].Priority }) + + if err := impl.DBService.Where("enabled = ?", true).Find(&shield).Error; err != nil { + return err + } + + e.mu.Lock() + e.trapDict = dict + e.syslogRules = syslog + e.trapRules = trap + e.shields = shield + e.mu.Unlock() + return nil +} + +func StartRefresher() { + interval := config.Spec.Ingest.RuleRefreshSecs + if interval <= 0 { + interval = 30 + } + _ = Global.Refresh() + go func() { + t := time.NewTicker(time.Duration(interval) * time.Second) + defer t.Stop() + for range t.C { + _ = Global.Refresh() + } + }() +} + +func normOID(s string) string { + s = strings.TrimSpace(s) + return strings.TrimPrefix(s, ".") +} + +func (e *Engine) HandleSyslog(addr *net.UDPAddr, payload []byte) { + parsed := parseSyslogPayload(payload) + device := parsed.Hostname + if device == "" { + device = addr.IP.String() + } + detailObj := map[string]interface{}{ + "priority": parsed.Priority, + "hostname": parsed.Hostname, + "tag": parsed.Tag, + "message": parsed.Message, + } + detailBytes, _ := json.Marshal(detailObj) + summary := formatSyslogSummary(parsed) + sev := syslogPriorityToSeverity(parsed.Priority) + + ev := models.LogEvent{ + SourceKind: "syslog", + RemoteAddr: addr.String(), + RawPayload: string(payload), + NormalizedSummary: summary, + NormalizedDetail: string(detailBytes), + DeviceName: device, + SeverityCode: sev, + } + + e.mu.RLock() + rules := e.syslogRules + e.mu.RUnlock() + + var matched *models.SyslogRule + for i := range rules { + if syslogRuleMatches(&rules[i], device, parsed.Message, parsed.RawLine) { + matched = &rules[i] + break + } + } + + if err := impl.DBService.Create(&ev).Error; err != nil { + return + } + + if matched == nil { + return + } + labels := map[string]string{ + "source": "syslog", + "device": device, + "rule_id": strconv.FormatUint(uint64(matched.ID), 10), + "rule_name": matched.Name, + "remote_addr": addr.String(), + } + rawObj := map[string]interface{}{ + "source": "syslog", + "received_at": time.Now().UTC().Format(time.RFC3339), + "source_ip": addr.IP.String(), + "rule_id": matched.ID, + "log_entry_id": ev.ID, + "raw_packet": string(payload), + "parsed": detailObj, + } + rawBytes, mErr := json.Marshal(rawObj) + if mErr != nil { + return + } + body := AlertReceiveBody{ + AlertName: matched.AlertName, + Summary: summary, + Description: summary, + SeverityCode: firstNonEmpty(matched.SeverityCode, sev), + Value: parsed.Message, + Labels: labels, + Agent: "logs-syslog", + PolicyID: matched.PolicyID, + RawData: rawBytes, + } + if err := forwardAlert(body); err == nil { + _ = impl.DBService.Model(&ev).Update("alert_sent", true).Error + } +} + +func syslogRuleMatches(rule *models.SyslogRule, device, message, rawLine string) bool { + if strings.TrimSpace(rule.DeviceNameContains) == "" && strings.TrimSpace(rule.KeywordRegex) == "" { + return false + } + deviceName := strings.ToLower(device) + contains := strings.ToLower(rule.DeviceNameContains) + if contains != "" && !strings.Contains(deviceName, contains) { + return false + } + if rule.KeywordRegex != "" { + re, err := regexp.Compile(rule.KeywordRegex) + if err != nil { + return false + } + if !re.MatchString(message) && !re.MatchString(rawLine) { + return false + } + } + return true +} + +func trapShielded(e *Engine, addr *net.UDPAddr, trapOID string, pkt *gosnmp.SnmpPacket) bool { + ip := addr.IP + fp := varbindFingerprint(pkt) + now := time.Now() + e.mu.RLock() + shields := e.shields + e.mu.RUnlock() + for i := range shields { + s := &shields[i] + if !s.Enabled { + continue + } + if strings.TrimSpace(s.SourceIPCIDR) == "" { + continue + } + if !ipMatchesCIDR(ip, s.SourceIPCIDR) { + continue + } + if p := strings.TrimSpace(s.OIDPrefix); p != "" && !strings.HasPrefix(normOID(trapOID), normOID(p)) { + continue + } + if h := strings.TrimSpace(s.InterfaceHint); h != "" && !strings.Contains(fp, h) { + continue + } + if !inTimeWindows(now, s.TimeWindowsJSON) { + continue + } + return true + } + return false +} + +func lookupTrapDict(e *Engine, trapOID string) *models.TrapDictionaryEntry { + t := normOID(trapOID) + e.mu.RLock() + dict := e.trapDict + e.mu.RUnlock() + for i := range dict { + if strings.HasPrefix(t, normOID(dict[i].OIDPrefix)) { + return &dict[i] + } + } + return nil +} + +func (e *Engine) HandleTrap(addr *net.UDPAddr, pkt *gosnmp.SnmpPacket) { + trapOID := extractTrapOID(pkt) + if trapShielded(e, addr, trapOID, pkt) { + return + } + + dict := lookupTrapDict(e, trapOID) + fp := varbindFingerprint(pkt) + vbJSON, _ := json.Marshal(trapVarbinds(pkt)) + + readable := buildTrapReadable(trapOID, dict, fp) + detailObj := map[string]interface{}{ + "trap_oid": trapOID, + "varbinds": trapVarbinds(pkt), + "dict_title": "", + "dict_description": "", + "recovery": "", + } + sev := "warning" + if dict != nil { + detailObj["dict_title"] = dict.Title + detailObj["dict_description"] = dict.Description + detailObj["recovery"] = dict.RecoveryMessage + if dict.SeverityCode != "" { + sev = dict.SeverityCode + } + } + detailBytes, _ := json.Marshal(detailObj) + + ev := models.LogEvent{ + SourceKind: "snmp_trap", + RemoteAddr: addr.String(), + RawPayload: fp, + NormalizedSummary: readable, + NormalizedDetail: string(detailBytes), + DeviceName: addr.IP.String(), + SeverityCode: sev, + TrapOID: trapOID, + } + if err := impl.DBService.Create(&ev).Error; err != nil { + return + } + + e.mu.RLock() + rules := e.trapRules + e.mu.RUnlock() + + var matched *models.TrapRule + for i := range rules { + if trapRuleMatches(&rules[i], trapOID, fp) { + matched = &rules[i] + break + } + } + + if matched == nil && dict != nil && strings.TrimSpace(dict.SeverityCode) != "" { + matched = &models.TrapRule{ + AlertName: firstNonEmpty(dict.Title, "SNMP Trap"), + SeverityCode: dict.SeverityCode, + PolicyID: 0, + } + } + if matched == nil { + return + } + + desc := readable + if dict != nil && dict.RecoveryMessage != "" { + desc = readable + "\n恢复建议: " + dict.RecoveryMessage + } + labels := map[string]string{ + "source": "snmp_trap", + "trap_oid": trapOID, + "remote_addr": addr.String(), + } + if matched.ID != 0 { + labels["rule_id"] = strconv.FormatUint(uint64(matched.ID), 10) + labels["rule_name"] = matched.Name + } + resolved := map[string]interface{}{} + if dict != nil { + resolved["title"] = dict.Title + resolved["description"] = dict.Description + resolved["recovery"] = dict.RecoveryMessage + } + rawObj := map[string]interface{}{ + "source": "snmp_trap", + "received_at": time.Now().UTC().Format(time.RFC3339), + "source_ip": addr.IP.String(), + "log_entry_id": ev.ID, + "trap_oid": trapOID, + "varbinds": trapVarbinds(pkt), + "resolved": resolved, + "pdu_summary": fp, + } + if matched.ID != 0 { + rawObj["rule_id"] = matched.ID + } + rawBytes, mErr := json.Marshal(rawObj) + if mErr != nil { + return + } + body := AlertReceiveBody{ + AlertName: firstNonEmpty(matched.AlertName, "SNMP Trap"), + Summary: readable, + Description: desc, + SeverityCode: firstNonEmpty(matched.SeverityCode, sev), + Value: string(vbJSON), + Labels: labels, + Agent: "logs-trap", + PolicyID: matched.PolicyID, + RawData: rawBytes, + } + if err := forwardAlert(body); err == nil { + _ = impl.DBService.Model(&ev).Update("alert_sent", true).Error + } +} + +func extractTrapOID(pkt *gosnmp.SnmpPacket) string { + const snmpTrapOID = "1.3.6.1.6.3.1.1.4.1.0" + for _, v := range pkt.Variables { + if v.Name == snmpTrapOID || strings.HasSuffix(v.Name, ".1.3.6.1.6.3.1.1.4.1.0") { + return oidToString(v.Value) + } + } + for _, v := range pkt.Variables { + if strings.Contains(v.Name, "1.3.6.1.6.3.1.1.4.1") { + return oidToString(v.Value) + } + } + return "" +} + +func oidToString(val interface{}) string { + switch x := val.(type) { + case string: + return x + case []byte: + return string(x) + default: + return fmt.Sprintf("%v", x) + } +} + +func trapVarbinds(pkt *gosnmp.SnmpPacket) []map[string]string { + out := make([]map[string]string, 0, len(pkt.Variables)) + for _, v := range pkt.Variables { + out = append(out, map[string]string{ + "oid": v.Name, + "type": fmt.Sprintf("%v", v.Type), + "value": fmtVarbindValue(v), + }) + } + return out +} + +func buildTrapReadable(trapOID string, dict *models.TrapDictionaryEntry, varbindSummary string) string { + if dict != nil && dict.Title != "" { + return dict.Title + " (" + trapOID + ")" + } + if trapOID != "" { + return "Trap " + trapOID + } + return truncate(varbindSummary, 256) +} + +func trapRuleMatches(rule *models.TrapRule, trapOID, varbindFP string) bool { + hasOID := strings.TrimSpace(rule.OIDPrefix) != "" + hasRE := strings.TrimSpace(rule.VarbindMatchRegex) != "" + if !hasOID && !hasRE { + return false + } + if hasOID && !strings.HasPrefix(normOID(trapOID), normOID(rule.OIDPrefix)) { + return false + } + if rule.VarbindMatchRegex != "" { + re, err := regexp.Compile(rule.VarbindMatchRegex) + if err != nil { + return false + } + if !re.MatchString(varbindFP) { + return false + } + } + return true +} + +func firstNonEmpty(a, b string) string { + if strings.TrimSpace(a) != "" { + return a + } + return b +} diff --git a/internal/ingest/shield.go b/internal/ingest/shield.go new file mode 100644 index 0000000..ef273ae --- /dev/null +++ b/internal/ingest/shield.go @@ -0,0 +1,114 @@ +package ingest + +import ( + "encoding/json" + "fmt" + "net" + "strconv" + "strings" + "time" + + "github.com/gosnmp/gosnmp" +) + +type timeWindow struct { + Days []int `json:"days"` + Start string `json:"start"` + End string `json:"end"` +} + +func ipMatchesCIDR(ip net.IP, cidr string) bool { + cidr = strings.TrimSpace(cidr) + if cidr == "" { + return false + } + if !strings.Contains(cidr, "/") { + p := net.ParseIP(cidr) + return p != nil && p.Equal(ip) + } + _, network, err := net.ParseCIDR(cidr) + if err != nil { + return false + } + return network.Contains(ip) +} + +func inTimeWindows(now time.Time, jsonStr string) bool { + s := strings.TrimSpace(jsonStr) + if s == "" || s == "null" { + return true + } + var windows []timeWindow + if err := json.Unmarshal([]byte(s), &windows); err != nil || len(windows) == 0 { + return true + } + tod := now.Hour()*60 + now.Minute() + wd := int(now.Weekday()) + for _, w := range windows { + if len(w.Days) > 0 { + ok := false + for _, d := range w.Days { + if d == wd { + ok = true + break + } + } + if !ok { + continue + } + } + start := parseHHMM(w.Start) + end := parseHHMM(w.End) + if start < 0 || end < 0 { + continue + } + if start <= end { + if tod >= start && tod <= end { + return true + } + } else { + if tod >= start || tod <= end { + return true + } + } + } + return false +} + +func parseHHMM(s string) int { + s = strings.TrimSpace(s) + if s == "" { + return -1 + } + parts := strings.Split(s, ":") + if len(parts) != 2 { + return -1 + } + h, err1 := strconv.Atoi(parts[0]) + m, err2 := strconv.Atoi(parts[1]) + if err1 != nil || err2 != nil || h < 0 || h > 23 || m < 0 || m > 59 { + return -1 + } + return h*60 + m +} + +func varbindFingerprint(pkt *gosnmp.SnmpPacket) string { + var b strings.Builder + for _, v := range pkt.Variables { + b.WriteString(v.Name) + b.WriteByte('=') + b.WriteString(fmtVarbindValue(v)) + b.WriteByte(';') + } + return b.String() +} + +func fmtVarbindValue(v gosnmp.SnmpPDU) string { + switch v.Type { + case gosnmp.OctetString: + if bb, ok := v.Value.([]byte); ok { + return string(bb) + } + } + return fmt.Sprintf("%v", v.Value) +} diff --git a/internal/ingest/syslog_parse.go b/internal/ingest/syslog_parse.go new file mode 100644 index 0000000..bb7b799 --- /dev/null +++ b/internal/ingest/syslog_parse.go @@ -0,0 +1,109 @@ +package ingest + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +var rePri = regexp.MustCompile(`^<(\d{1,3})>`) + +type ParsedSyslog struct { + Priority int + Hostname string + Tag string + Message string + RawLine string +} + +func parseSyslogPayload(payload []byte) ParsedSyslog { + line := strings.TrimSpace(string(payload)) + p := ParsedSyslog{RawLine: line, Message: line} + if line == "" { + return p + } + rest := line + if m := rePri.FindStringSubmatch(line); len(m) == 2 { + if pri, err := strconv.Atoi(m[1]); err == nil { + p.Priority = pri + } + rest = line[len(m[0]):] + } + rest = strings.TrimSpace(rest) + fields := strings.SplitN(rest, " ", 6) + if len(fields) >= 2 && len(fields[0]) == 1 && fields[0][0] >= '1' && fields[0][0] <= '9' { + if len(fields) >= 4 { + p.Hostname = fields[2] + if len(fields) >= 6 { + p.Message = fields[5] + } else if len(fields) == 5 { + p.Message = fields[4] + } + } + return p + } + tokens := strings.SplitN(rest, " ", 3) + if len(tokens) >= 2 { + if len(tokens) >= 3 && isMonthAbbr(tokens[0]) { + p.Hostname = tokens[2] + if idx := strings.Index(rest, ": "); idx > 0 { + p.Message = strings.TrimSpace(rest[idx+2:]) + } + } else { + p.Hostname = tokens[1] + if len(tokens) >= 3 { + tagMsg := tokens[2] + if idx := strings.Index(tagMsg, ": "); idx > 0 { + p.Tag = tagMsg[:idx] + p.Message = strings.TrimSpace(tagMsg[idx+2:]) + } else { + p.Message = tagMsg + } + } + } + } + return p +} + +func isMonthAbbr(s string) bool { + if len(s) < 3 { + return false + } + mons := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} + for _, m := range mons { + if strings.HasPrefix(s, m) { + return true + } + } + return false +} + +func syslogPriorityToSeverity(pri int) string { + sev := pri % 8 + switch sev { + case 0, 1, 2: + return "critical" + case 3: + return "major" + case 4: + return "warning" + default: + return "info" + } +} + +func formatSyslogSummary(p ParsedSyslog) string { + host := p.Hostname + if host == "" { + host = "unknown-host" + } + return fmt.Sprintf("%s: %s", host, truncate(p.Message, 512)) +} + +func truncate(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "..." +} diff --git a/internal/ingest/syslog_parse_test.go b/internal/ingest/syslog_parse_test.go new file mode 100644 index 0000000..39618dd --- /dev/null +++ b/internal/ingest/syslog_parse_test.go @@ -0,0 +1,32 @@ +package ingest + +import ( + "encoding/json" + "testing" +) + +func TestParseSyslogPayloadPri(t *testing.T) { + p := parseSyslogPayload([]byte("<34>Oct 11 22:14:15 mymachine su: 'su root' failed for lonvick on /dev/pts/8")) + if p.Priority != 34 { + t.Fatalf("priority=%d", p.Priority) + } +} + +func TestForwardAlertBodyIncludesRawData(t *testing.T) { + raw := []byte(`{"source":"syslog","parsed":{}}`) + b := AlertReceiveBody{ + AlertName: "a", + RawData: raw, + } + data, err := json.Marshal(b) + if err != nil { + t.Fatal(err) + } + var dec map[string]json.RawMessage + if err := json.Unmarshal(data, &dec); err != nil { + t.Fatal(err) + } + if string(dec["raw_data"]) != string(raw) { + t.Fatalf("raw_data %s", dec["raw_data"]) + } +} diff --git a/internal/ingest/syslog_udp.go b/internal/ingest/syslog_udp.go new file mode 100644 index 0000000..139bca9 --- /dev/null +++ b/internal/ingest/syslog_udp.go @@ -0,0 +1,41 @@ +package ingest + +import ( + "log" + "net" + "strings" + + "git.apinb.com/ops/logs/internal/config" +) + +func StartSyslogUDP() { + addr := strings.TrimSpace(config.Spec.Ingest.SyslogListenAddr) + if addr == "" { + return + } + go func() { + pc, err := net.ListenPacket("udp", addr) + if err != nil { + log.Printf("logs: syslog UDP listen %s: %v", addr, err) + return + } + defer pc.Close() + log.Printf("logs: syslog listening UDP %s", addr) + buf := make([]byte, 65536) + for { + n, remote, err := pc.ReadFrom(buf) + if err != nil { + log.Printf("logs: syslog read: %v", err) + continue + } + udpAddr, _ := remote.(*net.UDPAddr) + if udpAddr == nil { + continue + } + p := make([]byte, n) + copy(p, buf[:n]) + a := *udpAddr + Global.HandleSyslog(&a, p) + } + }() +} diff --git a/internal/ingest/trap_udp.go b/internal/ingest/trap_udp.go new file mode 100644 index 0000000..5a40364 --- /dev/null +++ b/internal/ingest/trap_udp.go @@ -0,0 +1,32 @@ +package ingest + +import ( + "log" + "net" + "strings" + + "git.apinb.com/ops/logs/internal/config" + "github.com/gosnmp/gosnmp" +) + +func StartTrapUDP() { + addr := strings.TrimSpace(config.Spec.Ingest.TrapListenAddr) + if addr == "" { + return + } + go func() { + tl := gosnmp.NewTrapListener() + tl.OnNewTrap = func(pkt *gosnmp.SnmpPacket, u *net.UDPAddr) { + if u == nil || pkt == nil { + return + } + ua := *u + Global.HandleTrap(&ua, pkt) + } + tl.Params = gosnmp.Default + tl.Params.Logger = gosnmp.NewLogger(log.Default()) + if err := tl.Listen(addr); err != nil { + log.Printf("logs: trap listener %s: %v", addr, err) + } + }() +} diff --git a/internal/logic/controllers/crud.go b/internal/logic/controllers/crud.go new file mode 100644 index 0000000..1529bdd --- /dev/null +++ b/internal/logic/controllers/crud.go @@ -0,0 +1,297 @@ +package controllers + +import ( + "errors" + "strconv" + + "git.apinb.com/bsm-sdk/core/infra" + "git.apinb.com/ops/logs/internal/impl" + "git.apinb.com/ops/logs/internal/ingest" + "git.apinb.com/ops/logs/internal/models" + "github.com/gin-gonic/gin" +) + +func parseID(ctx *gin.Context) (uint, error) { + id64, err := strconv.ParseUint(ctx.Param("id"), 10, 32) + if err != nil { + return 0, err + } + return uint(id64), nil +} + +func ListSyslogRules(ctx *gin.Context) { + var rows []models.SyslogRule + if err := impl.DBService.Order("priority desc, id asc").Find(&rows).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, gin.H{"items": rows}) +} + +func CreateSyslogRule(ctx *gin.Context) { + var row models.SyslogRule + if err := ctx.ShouldBindJSON(&row); err != nil { + infra.Response.Error(ctx, err) + return + } + row.ID = 0 + if err := impl.DBService.Create(&row).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, row) +} + +func UpdateSyslogRule(ctx *gin.Context) { + id, err := parseID(ctx) + if err != nil { + infra.Response.Error(ctx, errors.New("invalid id")) + return + } + var row models.SyslogRule + if err := impl.DBService.First(&row, id).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + if err := ctx.ShouldBindJSON(&row); err != nil { + infra.Response.Error(ctx, err) + return + } + row.ID = id + if err := impl.DBService.Save(&row).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, row) +} + +func DeleteSyslogRule(ctx *gin.Context) { + id, err := parseID(ctx) + if err != nil { + infra.Response.Error(ctx, errors.New("invalid id")) + return + } + if err := impl.DBService.Delete(&models.SyslogRule{}, id).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, gin.H{"deleted": id}) +} + +func ListTrapRules(ctx *gin.Context) { + var rows []models.TrapRule + if err := impl.DBService.Order("priority desc, id asc").Find(&rows).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, gin.H{"items": rows}) +} + +func CreateTrapRule(ctx *gin.Context) { + var row models.TrapRule + if err := ctx.ShouldBindJSON(&row); err != nil { + infra.Response.Error(ctx, err) + return + } + row.ID = 0 + if err := impl.DBService.Create(&row).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, row) +} + +func UpdateTrapRule(ctx *gin.Context) { + id, err := parseID(ctx) + if err != nil { + infra.Response.Error(ctx, errors.New("invalid id")) + return + } + var row models.TrapRule + if err := impl.DBService.First(&row, id).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + if err := ctx.ShouldBindJSON(&row); err != nil { + infra.Response.Error(ctx, err) + return + } + row.ID = id + if err := impl.DBService.Save(&row).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, row) +} + +func DeleteTrapRule(ctx *gin.Context) { + id, err := parseID(ctx) + if err != nil { + infra.Response.Error(ctx, errors.New("invalid id")) + return + } + if err := impl.DBService.Delete(&models.TrapRule{}, id).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, gin.H{"deleted": id}) +} + +func ListTrapDictionary(ctx *gin.Context) { + var rows []models.TrapDictionaryEntry + if err := impl.DBService.Order("id asc").Find(&rows).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, gin.H{"items": rows}) +} + +func CreateTrapDictionary(ctx *gin.Context) { + var row models.TrapDictionaryEntry + if err := ctx.ShouldBindJSON(&row); err != nil { + infra.Response.Error(ctx, err) + return + } + row.ID = 0 + if err := impl.DBService.Create(&row).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, row) +} + +func UpdateTrapDictionary(ctx *gin.Context) { + id, err := parseID(ctx) + if err != nil { + infra.Response.Error(ctx, errors.New("invalid id")) + return + } + var row models.TrapDictionaryEntry + if err := impl.DBService.First(&row, id).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + if err := ctx.ShouldBindJSON(&row); err != nil { + infra.Response.Error(ctx, err) + return + } + row.ID = id + if err := impl.DBService.Save(&row).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, row) +} + +func DeleteTrapDictionary(ctx *gin.Context) { + id, err := parseID(ctx) + if err != nil { + infra.Response.Error(ctx, errors.New("invalid id")) + return + } + if err := impl.DBService.Delete(&models.TrapDictionaryEntry{}, id).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, gin.H{"deleted": id}) +} + +func ListTrapShields(ctx *gin.Context) { + var rows []models.TrapShield + if err := impl.DBService.Order("id asc").Find(&rows).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, gin.H{"items": rows}) +} + +func CreateTrapShield(ctx *gin.Context) { + var row models.TrapShield + if err := ctx.ShouldBindJSON(&row); err != nil { + infra.Response.Error(ctx, err) + return + } + row.ID = 0 + if err := impl.DBService.Create(&row).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, row) +} + +func UpdateTrapShield(ctx *gin.Context) { + id, err := parseID(ctx) + if err != nil { + infra.Response.Error(ctx, errors.New("invalid id")) + return + } + var row models.TrapShield + if err := impl.DBService.First(&row, id).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + if err := ctx.ShouldBindJSON(&row); err != nil { + infra.Response.Error(ctx, err) + return + } + row.ID = id + if err := impl.DBService.Save(&row).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, row) +} + +func DeleteTrapShield(ctx *gin.Context) { + id, err := parseID(ctx) + if err != nil { + infra.Response.Error(ctx, errors.New("invalid id")) + return + } + if err := impl.DBService.Delete(&models.TrapShield{}, id).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + _ = ingest.Global.Refresh() + infra.Response.Success(ctx, gin.H{"deleted": id}) +} + +func ListLogEvents(ctx *gin.Context) { + kind := ctx.Query("source_kind") + page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) + size, _ := strconv.Atoi(ctx.DefaultQuery("page_size", "50")) + if page < 1 { + page = 1 + } + if size < 1 || size > 500 { + size = 50 + } + offset := (page - 1) * size + q := impl.DBService.Model(&models.LogEvent{}) + if kind != "" { + q = q.Where("source_kind = ?", kind) + } + var total int64 + _ = q.Count(&total).Error + var rows []models.LogEvent + if err := q.Order("id desc").Offset(offset).Limit(size).Find(&rows).Error; err != nil { + infra.Response.Error(ctx, err) + return + } + infra.Response.Success(ctx, gin.H{"total": total, "page": page, "page_size": size, "items": rows}) +} diff --git a/internal/logic/ping/ping.go b/internal/logic/ping/ping.go new file mode 100644 index 0000000..0e481a0 --- /dev/null +++ b/internal/logic/ping/ping.go @@ -0,0 +1,10 @@ +package ping + +import ( + "git.apinb.com/bsm-sdk/core/infra" + "github.com/gin-gonic/gin" +) + +func Hello(ctx *gin.Context) { + infra.Response.Success(ctx, gin.H{"service": "logs", "status": "ok"}) +} diff --git a/internal/models/log_event.go b/internal/models/log_event.go new file mode 100644 index 0000000..2064d81 --- /dev/null +++ b/internal/models/log_event.go @@ -0,0 +1,21 @@ +package models + +import "time" + +type LogEvent struct { + ID uint `gorm:"primaryKey" json:"id"` + CreatedAt time.Time `json:"created_at"` + SourceKind string `gorm:"size:16;index" json:"source_kind"` + RemoteAddr string `gorm:"size:64" json:"remote_addr"` + RawPayload string `gorm:"type:text" json:"raw_payload"` + NormalizedSummary string `gorm:"type:text" json:"normalized_summary"` + NormalizedDetail string `gorm:"type:text" json:"normalized_detail"` + DeviceName string `gorm:"size:512;index" json:"device_name"` + SeverityCode string `gorm:"size:32" json:"severity_code"` + TrapOID string `gorm:"size:512;index" json:"trap_oid"` + AlertSent bool `json:"alert_sent"` +} + +func (LogEvent) TableName() string { + return "logs_events" +} diff --git a/internal/models/query.go b/internal/models/query.go new file mode 100644 index 0000000..23861f5 --- /dev/null +++ b/internal/models/query.go @@ -0,0 +1,17 @@ +package models + +// GetAllModels 数据库迁移用模型列表 +func GetAllModels() []interface{} { + return []interface{}{ + &LogEvent{}, + &TrapDictionaryEntry{}, + &SyslogRule{}, + &TrapRule{}, + &TrapShield{}, + } +} + +// InitData 预留默认数据 +func InitData() error { + return nil +} diff --git a/internal/models/syslog_rule.go b/internal/models/syslog_rule.go new file mode 100644 index 0000000..849c46b --- /dev/null +++ b/internal/models/syslog_rule.go @@ -0,0 +1,21 @@ +package models + +import "time" + +type SyslogRule struct { + ID uint `gorm:"primaryKey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `gorm:"size:256" json:"name"` + Enabled bool `gorm:"default:true" json:"enabled"` + Priority int `gorm:"index" json:"priority"` + DeviceNameContains string `gorm:"size:512" json:"device_name_contains"` + KeywordRegex string `gorm:"size:512" json:"keyword_regex"` + AlertName string `gorm:"size:256" json:"alert_name"` + SeverityCode string `gorm:"size:32" json:"severity_code"` + PolicyID uint `json:"policy_id"` +} + +func (SyslogRule) TableName() string { + return "logs_syslog_rules" +} diff --git a/internal/models/trap_dictionary.go b/internal/models/trap_dictionary.go new file mode 100644 index 0000000..a950f52 --- /dev/null +++ b/internal/models/trap_dictionary.go @@ -0,0 +1,19 @@ +package models + +import "time" + +type TrapDictionaryEntry struct { + ID uint `gorm:"primaryKey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + OIDPrefix string `gorm:"size:512;uniqueIndex" json:"oid_prefix"` + Title string `gorm:"size:512" json:"title"` + Description string `gorm:"type:text" json:"description"` + SeverityCode string `gorm:"size:32" json:"severity_code"` + RecoveryMessage string `gorm:"type:text" json:"recovery_message"` + Enabled bool `gorm:"default:true" json:"enabled"` +} + +func (TrapDictionaryEntry) TableName() string { + return "logs_trap_dictionary" +} diff --git a/internal/models/trap_rule.go b/internal/models/trap_rule.go new file mode 100644 index 0000000..78a9353 --- /dev/null +++ b/internal/models/trap_rule.go @@ -0,0 +1,21 @@ +package models + +import "time" + +type TrapRule struct { + ID uint `gorm:"primaryKey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `gorm:"size:256" json:"name"` + Enabled bool `gorm:"default:true" json:"enabled"` + Priority int `gorm:"index" json:"priority"` + OIDPrefix string `gorm:"size:512" json:"oid_prefix"` + VarbindMatchRegex string `gorm:"size:512" json:"varbind_match_regex"` + AlertName string `gorm:"size:256" json:"alert_name"` + SeverityCode string `gorm:"size:32" json:"severity_code"` + PolicyID uint `json:"policy_id"` +} + +func (TrapRule) TableName() string { + return "logs_trap_rules" +} diff --git a/internal/models/trap_shield.go b/internal/models/trap_shield.go new file mode 100644 index 0000000..1a13ce4 --- /dev/null +++ b/internal/models/trap_shield.go @@ -0,0 +1,19 @@ +package models + +import "time" + +type TrapShield struct { + ID uint `gorm:"primaryKey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `gorm:"size:256" json:"name"` + Enabled bool `gorm:"default:true" json:"enabled"` + SourceIPCIDR string `gorm:"size:64" json:"source_ip_cidr"` + OIDPrefix string `gorm:"size:512" json:"oid_prefix"` + InterfaceHint string `gorm:"size:256" json:"interface_hint"` + TimeWindowsJSON string `gorm:"type:text" json:"time_windows_json"` +} + +func (TrapShield) TableName() string { + return "logs_trap_shields" +} diff --git a/internal/routers/register.go b/internal/routers/register.go new file mode 100644 index 0000000..c407ea9 --- /dev/null +++ b/internal/routers/register.go @@ -0,0 +1,44 @@ +package routers + +import ( + "fmt" + + "git.apinb.com/bsm-sdk/core/middleware" + "git.apinb.com/ops/logs/internal/logic/controllers" + "git.apinb.com/ops/logs/internal/logic/ping" + "github.com/gin-gonic/gin" +) + +func Register(srvKey string, engine *gin.Engine) { + v1 := fmt.Sprintf("/%s/%s", srvKey, "v1") + anon := engine.Group(v1) + { + anon.GET("/ping/hello", ping.Hello) + } + + api := engine.Group(v1) + api.Use(middleware.JwtAuth(true)) + { + api.GET("/syslog-rules", controllers.ListSyslogRules) + api.POST("/syslog-rules", controllers.CreateSyslogRule) + api.PUT("/syslog-rules/:id", controllers.UpdateSyslogRule) + api.DELETE("/syslog-rules/:id", controllers.DeleteSyslogRule) + + api.GET("/trap-rules", controllers.ListTrapRules) + api.POST("/trap-rules", controllers.CreateTrapRule) + api.PUT("/trap-rules/:id", controllers.UpdateTrapRule) + api.DELETE("/trap-rules/:id", controllers.DeleteTrapRule) + + api.GET("/trap-dictionary", controllers.ListTrapDictionary) + api.POST("/trap-dictionary", controllers.CreateTrapDictionary) + api.PUT("/trap-dictionary/:id", controllers.UpdateTrapDictionary) + api.DELETE("/trap-dictionary/:id", controllers.DeleteTrapDictionary) + + api.GET("/trap-suppressions", controllers.ListTrapShields) + api.POST("/trap-suppressions", controllers.CreateTrapShield) + api.PUT("/trap-suppressions/:id", controllers.UpdateTrapShield) + api.DELETE("/trap-suppressions/:id", controllers.DeleteTrapShield) + + api.GET("/entries", controllers.ListLogEvents) + } +} diff --git a/scripts/pack.ps1 b/scripts/pack.ps1 new file mode 100644 index 0000000..085be98 --- /dev/null +++ b/scripts/pack.ps1 @@ -0,0 +1,3 @@ +go env -w GOOS="linux" +go build -o ops-logs ./cmd/main/main.go +go env -w GOOS="windows" \ No newline at end of file