From e865af48551f12088de6061234550867da5ee72e Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 28 Dec 2025 20:14:21 +0800 Subject: [PATCH 1/9] feat: add Prometheus metrics --- go.mod | 13 ++++++++++++- go.sum | 27 ++++++++++++++++++++++++++- main.go | 38 +++++++++++++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d64b5e4..7ea344e 100644 --- a/go.mod +++ b/go.mod @@ -2,15 +2,26 @@ module github.com/database-playground/sqlrunner go 1.25 -require modernc.org/sqlite v1.41.0 +require ( + github.com/prometheus/client_golang v1.23.2 + modernc.org/sqlite v1.41.0 +) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/tools v0.40.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect mvdan.cc/gofumpt v0.9.1 // indirect ) diff --git a/go.sum b/go.sum index 774b1a4..75a3ddd 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +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/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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -12,22 +16,40 @@ 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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= @@ -39,8 +61,11 @@ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= diff --git a/main.go b/main.go index c97714d..a2e82d9 100644 --- a/main.go +++ b/main.go @@ -11,9 +11,34 @@ import ( "time" sqlrunner "github.com/database-playground/sqlrunner/lib" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" "golang.org/x/sync/singleflight" ) +var totalQueryCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "query_requests_total", + Help: "The total number of SQL query requests.", + }, + []string{"code", "method"}, +) + +var queryDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "query_duration_seconds", + Help: "The duration of the SQL query request.", + }, + []string{"code", "method"}, +) + +func init() { + prometheus.MustRegister( + totalQueryCounter, + queryDuration, + ) +} + func main() { addr := ":8080" if os.Getenv("PORT") != "" { @@ -26,7 +51,18 @@ func main() { _, _ = w.Write([]byte("OK")) }) - http.Handle("POST /query", service) + http.Handle( + "POST /query", + promhttp.InstrumentHandlerCounter( + totalQueryCounter, + promhttp.InstrumentHandlerDuration( + queryDuration, + service, + ), + ), + ) + + http.Handle("GET /metrics", promhttp.Handler()) slog.Info("Listening", slog.String("addr", addr)) if err := http.ListenAndServe(addr, nil); err != nil { From 723400bd8325cca747b88cb9e42acb3e2049f34f Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 28 Dec 2025 22:13:55 +0800 Subject: [PATCH 2/9] feat: implement OpenTelemetry tracing and logging --- README.md | 31 +++++++++ go.mod | 27 +++++++- go.sum | 63 ++++++++++++++++- main.go | 35 +++++++++- otelprovider.go | 175 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 325 insertions(+), 6 deletions(-) create mode 100644 otelprovider.go diff --git a/README.md b/README.md index 09d5555..a7e3ff2 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,37 @@ curl --request GET \ OK ``` +## Observability + +SQL Runner exports its metrics at the API endpoint `/metrics`. + +It supports configuring OpenTelemetry (tracing and logging) using the following environment variables: + +Here are some useful variables: + +- `OTEL_SERVICE_NAME`: Specify the name of this service (e.g., `sqlrunner-replica-1`). +- `OTEL_TRACES_EXPORTER`: Specify where the traces should be exported. + - Supported values: `console`, `otlp` + - Default: `console` +- `OTEL_LOGS_EXPORTER`: Specify where the logs should be exported. + - Supported values: `console`, `otlp` + - Default: `console` +- `OTEL_EXPORTER_OTLP_PROTOCOL`: Specify the default protocol for OTLP. + - Supported values: `grpc`, `http/protobuf` + - Default: `grpc` +- `OTEL_EXPORTER_OTLP_ENDPOINT`: Specify the default endpoint for OTLP. + - Example: `http://otlp-collector:4317` +- `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL`: Specify the default protocol for OTLP trace data. + - Supported values: `grpc`, `http/protobuf` + - Default: falls back to `OTEL_EXPORTER_OTLP_PROTOCOL` +- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`: Specify the default endpoint for OTLP trace data. + - Example: `http://victoriatraces:10428/insert/opentelemetry/v1/traces` +- `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL`: Specify the default protocol for OTLP log data. + - Supported values: `grpc`, `http/protobuf` + - Default: falls back to `OTEL_EXPORTER_OTLP_PROTOCOL` +- `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`: Specify the default endpoint for OTLP log data. + - Example: `http://victorialogs:9428/insert/opentelemetry/v1/traces` + ## License Apache-2.0. See [LICENSE](LICENSE) for details. diff --git a/go.mod b/go.mod index 7ea344e..c548a88 100644 --- a/go.mod +++ b/go.mod @@ -4,24 +4,49 @@ go 1.25 require ( github.com/prometheus/client_golang v1.23.2 + go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 + go.opentelemetry.io/otel v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 + go.opentelemetry.io/otel/log v0.15.0 + go.opentelemetry.io/otel/sdk v1.39.0 + go.opentelemetry.io/otel/sdk/log v0.15.0 modernc.org/sqlite v1.41.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/tools v0.40.0 // indirect - google.golang.org/protobuf v1.36.8 // 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.77.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect mvdan.cc/gofumpt v0.9.1 // indirect ) diff --git a/go.sum b/go.sum index 75a3ddd..23bca95 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,30 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 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/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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +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/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -46,6 +57,42 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +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/contrib/bridges/otelslog v0.14.0 h1:eypSOd+0txRKCXPNyqLPsbSfA0jULgJcGmSAdFAnrCM= +go.opentelemetry.io/contrib/bridges/otelslog v0.14.0/go.mod h1:CRGvIBL/aAxpQU34ZxyQVFlovVcp67s4cAmQu8Jh9mc= +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/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0/go.mod h1:JM31r0GGZ/GU94mX8hN4D8v6e40aFlUECSQ48HaLgHM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 h1:EKpiGphOYq3CYnIe2eX9ftUkyU+Y8Dtte8OaWyHJ4+I= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0/go.mod h1:nWFP7C+T8TygkTjJ7mAyEaFaE7wNfms3nV/vexZ6qt0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0 h1:0BSddrtQqLEylcErkeFrJBmwFzcqfQq9+/uxfTZq+HE= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0/go.mod h1:87sjYuAPzaRCtdd09GU5gM1U9wQLrrcYrm77mh5EBoc= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 h1:8UPA4IbVZxpsD76ihGOQiFml99GPAEZLohDXvqHdi6U= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0/go.mod h1:MZ1T/+51uIVKlRzGw1Fo46KEWThjlCBZKl2LzY5nv4g= +go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY= +go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4= +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/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE= +go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= +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.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= @@ -54,15 +101,27 @@ golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1i golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +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.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +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= diff --git a/main.go b/main.go index a2e82d9..6840084 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,8 @@ import ( "log/slog" "net/http" "os" + "os/signal" + "syscall" "time" sqlrunner "github.com/database-playground/sqlrunner/lib" @@ -40,11 +42,34 @@ func init() { } func main() { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + defer stop() + addr := ":8080" if os.Getenv("PORT") != "" { addr = ":" + os.Getenv("PORT") } + srv := &http.Server{ + Addr: ":8080", + Handler: nil, + } + + go func() { + <-ctx.Done() + slog.Info("Received signal to shutdown") + if err := srv.Shutdown(context.Background()); err != nil { + slog.Error("Shutdown failed", slog.Any("error", err)) + } + }() + + shutdown, err := setupOTelSDK(ctx) + if err != nil { + slog.Error("Failed to setup OpenTelemetry", slog.Any("error", err)) + os.Exit(1) + } + defer shutdown(context.Background()) + service := &SqlQueryService{} http.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -65,9 +90,13 @@ func main() { http.Handle("GET /metrics", promhttp.Handler()) slog.Info("Listening", slog.String("addr", addr)) - if err := http.ListenAndServe(addr, nil); err != nil { - slog.Error("ListenAndServe failed", slog.Any("error", err)) - os.Exit(1) + if err := srv.ListenAndServe(); err != nil { + if errors.Is(err, http.ErrServerClosed) { + slog.Info("Server closed") + } else { + slog.Error("ListenAndServe failed", slog.Any("error", err)) + os.Exit(1) + } } } diff --git a/otelprovider.go b/otelprovider.go new file mode 100644 index 0000000..50d997e --- /dev/null +++ b/otelprovider.go @@ -0,0 +1,175 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log/slog" + "os" + + "go.opentelemetry.io/contrib/bridges/otelslog" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/log/global" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/log" + "go.opentelemetry.io/otel/sdk/trace" +) + +// setupOTelSDK bootstraps the OpenTelemetry pipeline. +// If it does not return an error, make sure to call shutdown for proper cleanup. +func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) { + var shutdownFuncs []func(context.Context) error + var err error + + // shutdown calls cleanup functions registered via shutdownFuncs. + // The errors from the calls are joined. + // Each registered cleanup will be invoked once. + shutdown := func(ctx context.Context) error { + var err error + for _, fn := range shutdownFuncs { + err = errors.Join(err, fn(ctx)) + } + shutdownFuncs = nil + return err + } + + // handleErr calls shutdown for cleanup and makes sure that all errors are returned. + handleErr := func(inErr error) { + err = errors.Join(inErr, shutdown(ctx)) + } + + // Set up propagator. + prop := newPropagator() + otel.SetTextMapPropagator(prop) + + // Set up trace provider. + tracerProvider, err := newTracerProvider(ctx) + if err != nil { + handleErr(err) + return shutdown, err + } + shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) + otel.SetTracerProvider(tracerProvider) + + // Set up logger provider. + loggerProvider, err := newLoggerProvider(ctx) + if err != nil { + handleErr(err) + return shutdown, err + } + shutdownFuncs = append(shutdownFuncs, loggerProvider.Shutdown) + global.SetLoggerProvider(loggerProvider) + + slog.SetDefault(slog.New(otelslog.NewHandler("sqlrunner"))) + + return shutdown, err +} + +func newPropagator() propagation.TextMapPropagator { + return propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) +} + +func newTracerProvider(ctx context.Context) (*trace.TracerProvider, error) { + traceExporter, err := newTracerExporter(ctx) + if err != nil { + return nil, err + } + + tracerProvider := trace.NewTracerProvider( + trace.WithBatcher(traceExporter), + ) + return tracerProvider, nil +} + +func newTracerExporter(ctx context.Context) (trace.SpanExporter, error) { + exporter := os.Getenv("OTEL_TRACES_EXPORTER") + if exporter == "" { + exporter = "console" + } + + switch exporter { + case "console": + return stdouttrace.New(stdouttrace.WithPrettyPrint()) + case "otlp": + return newOtlpTracerExporter(ctx) + default: + return nil, fmt.Errorf("unsupported exporter: %s", exporter) + } +} + +func newOtlpTracerExporter(ctx context.Context) (trace.SpanExporter, error) { + protocol := os.Getenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL") + if protocol == "" { + protocol = os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") + if protocol == "" { + protocol = "grpc" + } + } + + switch protocol { + case "grpc": + return otlptracegrpc.New(ctx) + case "http/protobuf": + return otlptracehttp.New(ctx) + default: + return nil, fmt.Errorf("unsupported protocol: %s", protocol) + } +} + +func newLoggerProvider(ctx context.Context) (*log.LoggerProvider, error) { + logExporter, err := newLoggerExporter(ctx) + if err != nil { + return nil, err + } + + loggerProvider := log.NewLoggerProvider( + log.WithProcessor( + log.NewBatchProcessor(logExporter), + ), + ) + return loggerProvider, nil +} + +func newLoggerExporter(ctx context.Context) (log.Exporter, error) { + exporter := os.Getenv("OTEL_LOGS_EXPORTER") + if exporter == "" { + exporter = "console" + } + + switch exporter { + case "console": + return stdoutlog.New() + case "otlp": + return newOtlpLoggerExporter(ctx) + default: + return nil, fmt.Errorf("unsupported exporter: %s", exporter) + } +} + +func newOtlpLoggerExporter(ctx context.Context) (log.Exporter, error) { + protocol := os.Getenv("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL") + if protocol == "" { + protocol = os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") + if protocol == "" { + protocol = "grpc" + } + } + + switch protocol { + case "grpc": + return otlploggrpc.New(ctx) + case "http/protobuf": + return otlploghttp.New(ctx) + default: + return nil, fmt.Errorf("unsupported protocol: %s", protocol) + } +} From 6c6f9a9416e6f734c767eab6ff96434454e36af0 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 28 Dec 2025 22:45:21 +0800 Subject: [PATCH 3/9] feat: implement tracing --- go.mod | 36 ++++++++++++-- go.sum | 89 ++++++++++++++++++++++++++++++--- lib/otel.go | 5 ++ lib/sqlrunner.go | 8 +++ main.go | 126 +++++++++++++++++++---------------------------- 5 files changed, 176 insertions(+), 88 deletions(-) create mode 100644 lib/otel.go diff --git a/go.mod b/go.mod index c548a88..b1b73e9 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,10 @@ module github.com/database-playground/sqlrunner go 1.25 require ( - github.com/prometheus/client_golang v1.23.2 + github.com/Depado/ginprom v1.8.2 + github.com/gin-gonic/gin v1.11.0 go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 + go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0 go.opentelemetry.io/otel v1.39.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 @@ -20,24 +22,48 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.2 // indirect + github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // 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/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.1 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // 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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.58.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/arch v0.23.0 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.48.0 // indirect @@ -46,7 +72,7 @@ require ( 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.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect mvdan.cc/gofumpt v0.9.1 // indirect ) diff --git a/go.sum b/go.sum index 23bca95..44ff5c7 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,56 @@ +github.com/Depado/ginprom v1.8.2 h1:H3sXqXlHfXpoUHciuWSbod1jzc9OyaZ4edM5oYL/nUI= +github.com/Depado/ginprom v1.8.2/go.mod h1:uq9dl4TqwBr0OpkvswJURh5fmjZcbrrMoDiDFHN8dMw= +github.com/appleboy/gofight/v2 v2.2.0 h1:uqQ3wzTlF1ma+r4jRCQ4cygCjrGZyZEBMBCjT/t9zRw= +github.com/appleboy/gofight/v2 v2.2.0/go.mod h1:USTV3UbA5kHBs4I91EsPi+6PIVZAx3KLorYjvtON91A= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +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.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= +github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= +github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 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/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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +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/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 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-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +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.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE= +github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 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/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -27,40 +59,74 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLW github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +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.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug= +github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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.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= 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/contrib/bridges/otelslog v0.14.0 h1:eypSOd+0txRKCXPNyqLPsbSfA0jULgJcGmSAdFAnrCM= go.opentelemetry.io/contrib/bridges/otelslog v0.14.0/go.mod h1:CRGvIBL/aAxpQU34ZxyQVFlovVcp67s4cAmQu8Jh9mc= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0 h1:7IKZbAYwlwLXAdu7SVPhzTjDjogWZxP4MIa7rovY+PU= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.64.0/go.mod h1:+TF5nf3NIv2X8PGxqfYOaRnAoMM43rUA2C3XsN2DoWA= +go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk= +go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY= 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/exporters/otlp/otlplog/otlploggrpc v0.15.0 h1:W+m0g+/6v3pa5PgVf2xoFMi5YtNR06WtS7ve5pcvLtM= @@ -95,8 +161,14 @@ go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjce go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= @@ -120,11 +192,12 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= diff --git a/lib/otel.go b/lib/otel.go new file mode 100644 index 0000000..e6579ac --- /dev/null +++ b/lib/otel.go @@ -0,0 +1,5 @@ +package sqlrunner + +import "go.opentelemetry.io/otel" + +var tracer = otel.Tracer("sqlrunner.lib") diff --git a/lib/sqlrunner.go b/lib/sqlrunner.go index f72c1ff..42314f2 100644 --- a/lib/sqlrunner.go +++ b/lib/sqlrunner.go @@ -147,11 +147,17 @@ func NewSQLRunner(schema string) (*SQLRunner, error) { // Query executes a query and returns the result. func (r *SQLRunner) Query(ctx context.Context, query string) (*QueryResult, error) { + _, span := tracer.Start(ctx, "SQLRunner.Query") + defer span.End() + + span.AddEvent("cache.get") // Check the cache first if result, ok := r.cache.Get(query); ok { + span.AddEvent("cache.hit") return result, nil } + span.AddEvent("sqlite.open") db, err := r.getSqliteInstance() if err != nil { return nil, fmt.Errorf("get schema: %w", err) @@ -162,6 +168,7 @@ func (r *SQLRunner) Query(ctx context.Context, query string) (*QueryResult, erro } }() + span.AddEvent("sqlite.query") result, err := db.QueryContext(ctx, query) if err != nil { return nil, NewQueryError(err) @@ -172,6 +179,7 @@ func (r *SQLRunner) Query(ctx context.Context, query string) (*QueryResult, erro } }() + span.AddEvent("construct_result") cols, err := result.Columns() if err != nil { return nil, fmt.Errorf("get columns: %w", err) diff --git a/main.go b/main.go index 6840084..5f77d84 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/json" "errors" "fmt" "log/slog" @@ -12,34 +11,16 @@ import ( "syscall" "time" + "github.com/Depado/ginprom" sqlrunner "github.com/database-playground/sqlrunner/lib" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/gin-gonic/gin" + "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" "golang.org/x/sync/singleflight" ) -var totalQueryCounter = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "query_requests_total", - Help: "The total number of SQL query requests.", - }, - []string{"code", "method"}, -) - -var queryDuration = prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Name: "query_duration_seconds", - Help: "The duration of the SQL query request.", - }, - []string{"code", "method"}, -) - -func init() { - prometheus.MustRegister( - totalQueryCounter, - queryDuration, - ) -} +var tracer = otel.Tracer("sqlrunner") func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT) @@ -50,19 +31,6 @@ func main() { addr = ":" + os.Getenv("PORT") } - srv := &http.Server{ - Addr: ":8080", - Handler: nil, - } - - go func() { - <-ctx.Done() - slog.Info("Received signal to shutdown") - if err := srv.Shutdown(context.Background()); err != nil { - slog.Error("Shutdown failed", slog.Any("error", err)) - } - }() - shutdown, err := setupOTelSDK(ctx) if err != nil { slog.Error("Failed to setup OpenTelemetry", slog.Any("error", err)) @@ -70,33 +38,37 @@ func main() { } defer shutdown(context.Background()) - service := &SqlQueryService{} - http.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("OK")) - }) - - http.Handle( - "POST /query", - promhttp.InstrumentHandlerCounter( - totalQueryCounter, - promhttp.InstrumentHandlerDuration( - queryDuration, - service, - ), - ), + r := gin.Default() + p := ginprom.New( + ginprom.Engine(r), + ginprom.Path("/metrics"), ) + r.Use(p.Instrument()) + r.Use(otelgin.Middleware("sqlrunner")) + + srv := &http.Server{ + Addr: addr, + Handler: r, + } - http.Handle("GET /metrics", promhttp.Handler()) + r.GET("/healthz", func(c *gin.Context) { + c.String(http.StatusOK, "OK") + }) + + service := &SqlQueryService{} + r.POST("/query", service.Serve) - slog.Info("Listening", slog.String("addr", addr)) - if err := srv.ListenAndServe(); err != nil { - if errors.Is(err, http.ErrServerClosed) { - slog.Info("Server closed") - } else { + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { slog.Error("ListenAndServe failed", slog.Any("error", err)) - os.Exit(1) + panic(err) } + }() + + <-ctx.Done() + slog.Info("Received signal to shutdown") + if err := srv.Shutdown(context.Background()); err != nil { + slog.Error("Shutdown failed", slog.Any("error", err)) } } @@ -104,35 +76,45 @@ type SqlQueryService struct { sfgroup singleflight.Group } -func (s *SqlQueryService) ServeHTTP(w http.ResponseWriter, r *http.Request) { - decoder := json.NewDecoder(r.Body) +func (s *SqlQueryService) Serve(c *gin.Context) { + ctx, span := tracer.Start(c.Request.Context(), "SqlQueryService.Serve") + defer span.End() + var req QueryRequest - if err := decoder.Decode(&req); err != nil { - respond(w, http.StatusUnprocessableEntity, NewFailedResponse(BadPayloadError{Parent: err})) + if err := c.ShouldBindJSON(&req); err != nil { + span.SetStatus(codes.Error, "bad payload") + span.RecordError(err) + + c.JSON(http.StatusUnprocessableEntity, NewFailedResponse(BadPayloadError{Parent: err})) return } if req.Schema == "" || req.Query == "" { - respond(w, http.StatusUnprocessableEntity, NewFailedResponse(NewBadPayloadError("Schema and Query is required"))) + span.SetStatus(codes.Error, "bad payload") + span.RecordError(errors.New("schema and query are required")) + + c.JSON(http.StatusUnprocessableEntity, NewFailedResponse(NewBadPayloadError("Schema and Query is required"))) return } + span.AddEvent("runner.find") runner, err := s.findRunner(req.Schema) if err != nil { - respond(w, http.StatusInternalServerError, NewFailedResponse(err)) + c.JSON(http.StatusInternalServerError, NewFailedResponse(err)) return } - queryCtx, cancel := context.WithTimeout(r.Context(), time.Minute) + queryCtx, cancel := context.WithTimeout(ctx, time.Minute) defer cancel() + span.AddEvent("runner.query") result, err := runner.Query(queryCtx, req.Query) if err != nil { - respond(w, http.StatusBadRequest, NewFailedResponse(err)) + c.JSON(http.StatusBadRequest, NewFailedResponse(err)) return } - respond(w, http.StatusOK, NewSuccessResponse(result)) + c.JSON(http.StatusOK, NewSuccessResponse(result)) } func (s *SqlQueryService) findRunner(schema string) (*sqlrunner.SQLRunner, error) { @@ -212,9 +194,3 @@ func NewBadPayloadError(message string) BadPayloadError { func (e BadPayloadError) Error() string { return "bad payload: " + e.Parent.Error() } - -func respond(w http.ResponseWriter, status int, data any) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(data) -} From a5858b33fdbc5507a020590786c677f21a73ca13 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 28 Dec 2025 22:51:48 +0800 Subject: [PATCH 4/9] feat: add query_duration_seconds metric --- main.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 5f77d84..1662019 100644 --- a/main.go +++ b/main.go @@ -46,6 +46,8 @@ func main() { r.Use(p.Instrument()) r.Use(otelgin.Middleware("sqlrunner")) + p.AddCustomHistogram("query_duration_seconds", "The duration of the SQL query request.", []string{"code"}) + srv := &http.Server{ Addr: addr, Handler: r, @@ -55,7 +57,10 @@ func main() { c.String(http.StatusOK, "OK") }) - service := &SqlQueryService{} + service := &SqlQueryService{ + p: p, + sfgroup: singleflight.Group{}, + } r.POST("/query", service.Serve) go func() { @@ -73,10 +78,13 @@ func main() { } type SqlQueryService struct { + p *ginprom.Prometheus sfgroup singleflight.Group } func (s *SqlQueryService) Serve(c *gin.Context) { + now := time.Now() + ctx, span := tracer.Start(c.Request.Context(), "SqlQueryService.Serve") defer span.End() @@ -85,6 +93,7 @@ func (s *SqlQueryService) Serve(c *gin.Context) { span.SetStatus(codes.Error, "bad payload") span.RecordError(err) + s.p.AddCustomHistogramValue("query_duration_seconds", []string{"422"}, time.Since(now).Seconds()) c.JSON(http.StatusUnprocessableEntity, NewFailedResponse(BadPayloadError{Parent: err})) return } @@ -93,6 +102,7 @@ func (s *SqlQueryService) Serve(c *gin.Context) { span.SetStatus(codes.Error, "bad payload") span.RecordError(errors.New("schema and query are required")) + s.p.AddCustomHistogramValue("query_duration_seconds", []string{"422"}, time.Since(now).Seconds()) c.JSON(http.StatusUnprocessableEntity, NewFailedResponse(NewBadPayloadError("Schema and Query is required"))) return } @@ -100,6 +110,7 @@ func (s *SqlQueryService) Serve(c *gin.Context) { span.AddEvent("runner.find") runner, err := s.findRunner(req.Schema) if err != nil { + s.p.AddCustomHistogramValue("query_duration_seconds", []string{"500"}, time.Since(now).Seconds()) c.JSON(http.StatusInternalServerError, NewFailedResponse(err)) return } @@ -110,10 +121,12 @@ func (s *SqlQueryService) Serve(c *gin.Context) { span.AddEvent("runner.query") result, err := runner.Query(queryCtx, req.Query) if err != nil { + s.p.AddCustomHistogramValue("query_duration_seconds", []string{"400"}, time.Since(now).Seconds()) c.JSON(http.StatusBadRequest, NewFailedResponse(err)) return } + s.p.AddCustomHistogramValue("query_duration_seconds", []string{"200"}, time.Since(now).Seconds()) c.JSON(http.StatusOK, NewSuccessResponse(result)) } From d8b907e32c48d7b1b42c8295df82c17414952204 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 28 Dec 2025 22:55:45 +0800 Subject: [PATCH 5/9] fix: issues indicated by Copilot --- README.md | 2 +- lib/sqlrunner.go | 17 ++++++++++++++++- main.go | 22 +++++++++++++++++++--- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a7e3ff2..8135a5d 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Here are some useful variables: - Supported values: `grpc`, `http/protobuf` - Default: falls back to `OTEL_EXPORTER_OTLP_PROTOCOL` - `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`: Specify the default endpoint for OTLP log data. - - Example: `http://victorialogs:9428/insert/opentelemetry/v1/traces` + - Example: `http://victorialogs:9428/insert/opentelemetry/v1/logs` ## License diff --git a/lib/sqlrunner.go b/lib/sqlrunner.go index 42314f2..5e25027 100644 --- a/lib/sqlrunner.go +++ b/lib/sqlrunner.go @@ -17,6 +17,7 @@ import ( "time" lru "github.com/hashicorp/golang-lru/v2" + "go.opentelemetry.io/otel/codes" "golang.org/x/sync/singleflight" "modernc.org/sqlite" _ "modernc.org/sqlite" @@ -153,13 +154,16 @@ func (r *SQLRunner) Query(ctx context.Context, query string) (*QueryResult, erro span.AddEvent("cache.get") // Check the cache first if result, ok := r.cache.Get(query); ok { - span.AddEvent("cache.hit") + span.SetStatus(codes.Ok, "cache hit") return result, nil } span.AddEvent("sqlite.open") db, err := r.getSqliteInstance() if err != nil { + span.SetStatus(codes.Error, "get schema error") + span.RecordError(err) + return nil, fmt.Errorf("get schema: %w", err) } defer func() { @@ -171,6 +175,9 @@ func (r *SQLRunner) Query(ctx context.Context, query string) (*QueryResult, erro span.AddEvent("sqlite.query") result, err := db.QueryContext(ctx, query) if err != nil { + span.SetStatus(codes.Error, "query error") + span.RecordError(err) + return nil, NewQueryError(err) } defer func() { @@ -182,6 +189,9 @@ func (r *SQLRunner) Query(ctx context.Context, query string) (*QueryResult, erro span.AddEvent("construct_result") cols, err := result.Columns() if err != nil { + span.SetStatus(codes.Error, "get columns error") + span.RecordError(err) + return nil, fmt.Errorf("get columns: %w", err) } @@ -193,6 +203,9 @@ func (r *SQLRunner) Query(ctx context.Context, query string) (*QueryResult, erro } if err := result.Scan(rawCells...); err != nil { + span.SetStatus(codes.Error, "scan error") + span.RecordError(err) + return nil, fmt.Errorf("scan: %w", err) } @@ -210,8 +223,10 @@ func (r *SQLRunner) Query(ctx context.Context, query string) (*QueryResult, erro } // Add the result to the cache + span.AddEvent("cache.set") r.cache.Add(query, queryResult) + span.SetStatus(codes.Ok, "success") return queryResult, nil } diff --git a/main.go b/main.go index 1662019..08ce904 100644 --- a/main.go +++ b/main.go @@ -36,7 +36,13 @@ func main() { slog.Error("Failed to setup OpenTelemetry", slog.Any("error", err)) os.Exit(1) } - defer shutdown(context.Background()) + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := shutdown(ctx); err != nil { + slog.Error("Failed to shutdown OpenTelemetry", slog.Any("error", err)) + } + }() r := gin.Default() p := ginprom.New( @@ -72,7 +78,10 @@ func main() { <-ctx.Done() slog.Info("Received signal to shutdown") - if err := srv.Shutdown(context.Background()); err != nil { + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(shutdownCtx); err != nil { slog.Error("Shutdown failed", slog.Any("error", err)) } } @@ -103,13 +112,16 @@ func (s *SqlQueryService) Serve(c *gin.Context) { span.RecordError(errors.New("schema and query are required")) s.p.AddCustomHistogramValue("query_duration_seconds", []string{"422"}, time.Since(now).Seconds()) - c.JSON(http.StatusUnprocessableEntity, NewFailedResponse(NewBadPayloadError("Schema and Query is required"))) + c.JSON(http.StatusUnprocessableEntity, NewFailedResponse(NewBadPayloadError("Schema and Query are required"))) return } span.AddEvent("runner.find") runner, err := s.findRunner(req.Schema) if err != nil { + span.SetStatus(codes.Error, "runner find error") + span.RecordError(err) + s.p.AddCustomHistogramValue("query_duration_seconds", []string{"500"}, time.Since(now).Seconds()) c.JSON(http.StatusInternalServerError, NewFailedResponse(err)) return @@ -121,11 +133,15 @@ func (s *SqlQueryService) Serve(c *gin.Context) { span.AddEvent("runner.query") result, err := runner.Query(queryCtx, req.Query) if err != nil { + span.SetStatus(codes.Error, "query error") + span.RecordError(err) + s.p.AddCustomHistogramValue("query_duration_seconds", []string{"400"}, time.Since(now).Seconds()) c.JSON(http.StatusBadRequest, NewFailedResponse(err)) return } + span.SetStatus(codes.Ok, "success") s.p.AddCustomHistogramValue("query_duration_seconds", []string{"200"}, time.Since(now).Seconds()) c.JSON(http.StatusOK, NewSuccessResponse(result)) } From 43830986413830b6e490b6b5e4bdff912e436028 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 28 Dec 2025 22:58:44 +0800 Subject: [PATCH 6/9] feat: add query_requests_total --- main.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 08ce904..9197248 100644 --- a/main.go +++ b/main.go @@ -52,7 +52,8 @@ func main() { r.Use(p.Instrument()) r.Use(otelgin.Middleware("sqlrunner")) - p.AddCustomHistogram("query_duration_seconds", "The duration of the SQL query request.", []string{"code"}) + p.AddCustomHistogram("query_requests_total", "The total number of SQL query requests.", []string{"code"}) + p.AddCustomHistogram("query_requests_duration_seconds", "The duration of each SQL query request.", []string{"code"}) srv := &http.Server{ Addr: addr, @@ -102,7 +103,8 @@ func (s *SqlQueryService) Serve(c *gin.Context) { span.SetStatus(codes.Error, "bad payload") span.RecordError(err) - s.p.AddCustomHistogramValue("query_duration_seconds", []string{"422"}, time.Since(now).Seconds()) + s.p.IncrementCounterValue("query_requests_total", []string{"422"}) + s.p.AddCustomHistogramValue("query_requests_duration_seconds", []string{"422"}, time.Since(now).Seconds()) c.JSON(http.StatusUnprocessableEntity, NewFailedResponse(BadPayloadError{Parent: err})) return } @@ -111,7 +113,8 @@ func (s *SqlQueryService) Serve(c *gin.Context) { span.SetStatus(codes.Error, "bad payload") span.RecordError(errors.New("schema and query are required")) - s.p.AddCustomHistogramValue("query_duration_seconds", []string{"422"}, time.Since(now).Seconds()) + s.p.IncrementCounterValue("query_requests_total", []string{"422"}) + s.p.AddCustomHistogramValue("query_requests_duration_seconds", []string{"422"}, time.Since(now).Seconds()) c.JSON(http.StatusUnprocessableEntity, NewFailedResponse(NewBadPayloadError("Schema and Query are required"))) return } @@ -122,7 +125,8 @@ func (s *SqlQueryService) Serve(c *gin.Context) { span.SetStatus(codes.Error, "runner find error") span.RecordError(err) - s.p.AddCustomHistogramValue("query_duration_seconds", []string{"500"}, time.Since(now).Seconds()) + s.p.IncrementCounterValue("query_requests_total", []string{"500"}) + s.p.AddCustomHistogramValue("query_requests_duration_seconds", []string{"500"}, time.Since(now).Seconds()) c.JSON(http.StatusInternalServerError, NewFailedResponse(err)) return } @@ -136,13 +140,16 @@ func (s *SqlQueryService) Serve(c *gin.Context) { span.SetStatus(codes.Error, "query error") span.RecordError(err) - s.p.AddCustomHistogramValue("query_duration_seconds", []string{"400"}, time.Since(now).Seconds()) + s.p.IncrementCounterValue("query_requests_total", []string{"400"}) + s.p.AddCustomHistogramValue("query_requests_duration_seconds", []string{"400"}, time.Since(now).Seconds()) c.JSON(http.StatusBadRequest, NewFailedResponse(err)) return } + s.p.IncrementCounterValue("query_requests_total", []string{"200"}) + s.p.AddCustomHistogramValue("query_requests_duration_seconds", []string{"200"}, time.Since(now).Seconds()) span.SetStatus(codes.Ok, "success") - s.p.AddCustomHistogramValue("query_duration_seconds", []string{"200"}, time.Since(now).Seconds()) + c.JSON(http.StatusOK, NewSuccessResponse(result)) } From 2abd55cf71d7020c1b515546ee56d7f3fc5fcbd9 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Mon, 29 Dec 2025 00:13:50 +0800 Subject: [PATCH 7/9] refactor: abstract metric recorder --- main.go | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/main.go b/main.go index 9197248..bd6f68a 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "os/signal" + "strconv" "syscall" "time" @@ -71,6 +72,7 @@ func main() { r.POST("/query", service.Serve) go func() { + slog.Info("Starting server", slog.String("address", addr)) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { slog.Error("ListenAndServe failed", slog.Any("error", err)) panic(err) @@ -93,18 +95,17 @@ type SqlQueryService struct { } func (s *SqlQueryService) Serve(c *gin.Context) { - now := time.Now() - ctx, span := tracer.Start(c.Request.Context(), "SqlQueryService.Serve") defer span.End() + recordMetrics := s.createRecordMetricsFunc() + var req QueryRequest if err := c.ShouldBindJSON(&req); err != nil { span.SetStatus(codes.Error, "bad payload") span.RecordError(err) - s.p.IncrementCounterValue("query_requests_total", []string{"422"}) - s.p.AddCustomHistogramValue("query_requests_duration_seconds", []string{"422"}, time.Since(now).Seconds()) + recordMetrics(http.StatusUnprocessableEntity) c.JSON(http.StatusUnprocessableEntity, NewFailedResponse(BadPayloadError{Parent: err})) return } @@ -113,9 +114,8 @@ func (s *SqlQueryService) Serve(c *gin.Context) { span.SetStatus(codes.Error, "bad payload") span.RecordError(errors.New("schema and query are required")) - s.p.IncrementCounterValue("query_requests_total", []string{"422"}) - s.p.AddCustomHistogramValue("query_requests_duration_seconds", []string{"422"}, time.Since(now).Seconds()) - c.JSON(http.StatusUnprocessableEntity, NewFailedResponse(NewBadPayloadError("Schema and Query are required"))) + recordMetrics(http.StatusUnprocessableEntity) + c.JSON(http.StatusUnprocessableEntity, NewFailedResponse(NewBadPayloadError("schema and query are required"))) return } @@ -125,8 +125,7 @@ func (s *SqlQueryService) Serve(c *gin.Context) { span.SetStatus(codes.Error, "runner find error") span.RecordError(err) - s.p.IncrementCounterValue("query_requests_total", []string{"500"}) - s.p.AddCustomHistogramValue("query_requests_duration_seconds", []string{"500"}, time.Since(now).Seconds()) + recordMetrics(http.StatusInternalServerError) c.JSON(http.StatusInternalServerError, NewFailedResponse(err)) return } @@ -140,19 +139,26 @@ func (s *SqlQueryService) Serve(c *gin.Context) { span.SetStatus(codes.Error, "query error") span.RecordError(err) - s.p.IncrementCounterValue("query_requests_total", []string{"400"}) - s.p.AddCustomHistogramValue("query_requests_duration_seconds", []string{"400"}, time.Since(now).Seconds()) + recordMetrics(http.StatusBadRequest) c.JSON(http.StatusBadRequest, NewFailedResponse(err)) return } - s.p.IncrementCounterValue("query_requests_total", []string{"200"}) - s.p.AddCustomHistogramValue("query_requests_duration_seconds", []string{"200"}, time.Since(now).Seconds()) + recordMetrics(http.StatusOK) span.SetStatus(codes.Ok, "success") c.JSON(http.StatusOK, NewSuccessResponse(result)) } +func (s *SqlQueryService) createRecordMetricsFunc() func(code int) { + now := time.Now() + + return func(code int) { + s.p.IncrementCounterValue("query_requests_total", []string{strconv.Itoa(code)}) + s.p.AddCustomHistogramValue("query_requests_duration_seconds", []string{strconv.Itoa(code)}, time.Since(now).Seconds()) + } +} + func (s *SqlQueryService) findRunner(schema string) (*sqlrunner.SQLRunner, error) { result, err, _ := s.sfgroup.Do(schema, func() (any, error) { newRunner, err := sqlrunner.NewSQLRunner(schema) From d0e9739d59bce4b1b669d5dc14426a5b66974303 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Mon, 29 Dec 2025 00:14:21 +0800 Subject: [PATCH 8/9] fix: AddCustomCounter --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index bd6f68a..836f957 100644 --- a/main.go +++ b/main.go @@ -53,7 +53,7 @@ func main() { r.Use(p.Instrument()) r.Use(otelgin.Middleware("sqlrunner")) - p.AddCustomHistogram("query_requests_total", "The total number of SQL query requests.", []string{"code"}) + p.AddCustomCounter("query_requests_total", "The total number of SQL query requests.", []string{"code"}) p.AddCustomHistogram("query_requests_duration_seconds", "The duration of each SQL query request.", []string{"code"}) srv := &http.Server{ From 70460368cae5a903a1cf537d78b9488cc5e90e75 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Mon, 29 Dec 2025 00:15:30 +0800 Subject: [PATCH 9/9] feat: add a trace ID header --- main.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/main.go b/main.go index 836f957..7f9572f 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" "golang.org/x/sync/singleflight" ) @@ -53,6 +54,13 @@ func main() { r.Use(p.Instrument()) r.Use(otelgin.Middleware("sqlrunner")) + // Add a middleware to add the trace ID to the response header + r.Use(func(c *gin.Context) { + traceID := trace.SpanContextFromContext(c.Request.Context()).TraceID().String() + c.Header("X-Trace-ID", traceID) + c.Next() + }) + p.AddCustomCounter("query_requests_total", "The total number of SQL query requests.", []string{"code"}) p.AddCustomHistogram("query_requests_duration_seconds", "The duration of each SQL query request.", []string{"code"})