diff --git a/Makefile b/Makefile index 636f423..42dc846 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,12 @@ update: go get github.com/swaggo/gin-swagger@latest go get github.com/swaggo/swag@latest go get go.uber.org/zap@latest + go get go.opentelemetry.io/otel@latest + go get go.opentelemetry.io/otel/sdk@latest + go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp@latest + go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc@latest + go get go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin@latest + go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp@latest go get golang.org/x/net@latest go get golang.org/x/crypto@latest go get golang.org/x/sys@latest diff --git a/README.md b/README.md index b770315..99c9769 100644 --- a/README.md +++ b/README.md @@ -378,7 +378,22 @@ curl -sS localhost:8080/version | jq # {"version":"...","gitHash":"...","hostname":"..."} ``` -### 12. From inside the cluster (with cluster-utils shell) +### 12. Traces + Istio-style request ids + +```bash +# simulate gateway/mesh headers +curl -sS -D- \ + -H 'X-Request-Id: istio-style-id-001' \ + -H 'traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01' \ + localhost:8080/debug -o /dev/null | grep -iE 'x-trace-id|x-request-id' + +# with OTEL enabled, X-Trace-Id is the Tempo id; X-Request-Id echoes the mesh id +# logs: {"msg":"Request","trace_id":"...","request_id":"istio-style-id-001",...} +``` + +See **Observability** for push vs scrape and full header table. + +### 13. From inside the cluster (with cluster-utils shell) ```bash # port-forward @@ -472,7 +487,8 @@ curl -sS -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/ | `GET /ping` | `PONG` (not a kube probe) | | `GET /headers` | request headers | | `GET /debug` | hostname / ip / headers / uri | -| `GET /metrics` | prometheus (OpenMetrics): request count/latency/in-flight + Go/process | +| `GET /metrics` | prometheus **scrape** (OpenMetrics): request count/latency/in-flight + Go/process | +| OTEL traces | **push** OTLP to Alloy/collector (not scrape) — see Observability | | `GET /status/:code` | respond with that http status (100-599) | | `GET /delay/:seconds` | sleep then 200 (cap `MAX_DELAY_SECONDS`, default 120) | | `ANY /echo` | bounce method / query / headers / body | @@ -490,6 +506,134 @@ curl -sS -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/ --- +## Observability (metrics vs traces) + +Two different pipelines — don't mix them up: + +| Signal | How it leaves the app | Endpoint / protocol | Typical sink | +|--------|----------------------|---------------------|--------------| +| **Metrics** | **Scrape** (pull) | `GET /metrics` Prometheus/OpenMetrics | Alloy `prometheus.scrape` → Mimir/Prometheus | +| **Traces** | **Push** | **OTLP** http/protobuf (default) or grpc | Alloy OTLP receiver → **Tempo** | +| **Logs** | stdout JSON (zap) | not OTLP yet | Alloy/loki.source.kubernetes → Loki | + +Traces are **not** scraped. The app **exports** spans to a collector. Grafana Alloy is the usual middle hop: receive OTLP → forward to Tempo. + +### How end-to-end tracing works (Istio / meshes) + +Normal path in 2024–26 stacks: + +1. **Edge / sidecar (Envoy, Istio, Linkerd)** accepts the request and either + - continues an existing **W3C `traceparent`**, or + - creates/propagates **B3** (`x-b3-traceid`, …) if the mesh is still on Zipkin-style config +2. **App SDKs** extract that context, create child spans, inject the same headers on outbound calls +3. App **pushes** spans via OTLP → Alloy → Tempo +4. **`x-request-id`** (Envoy/Istio) is a **separate correlation id** used in access logs — it is *not* the OTEL trace id. Join them by putting `x-request-id` on the span (we do) and echoing both on the response. + +| Header | What it is | We do | +|--------|------------|--------| +| `traceparent` / `tracestate` | W3C trace context (modern default) | extract + inject | +| `x-b3-*` / `b3` | Zipkin B3 (common with Istio) | extract + inject | +| `uber-trace-id` | Jaeger | extract + inject | +| `x-request-id` | Envoy request id (logs) | span attr `http.request_id` + response echo | +| `x-correlation-id` | app/gateway variant | span attr + echo if present | + +So: we **match** mesh traffic by speaking **W3C + B3 + Jaeger**, and we **use** Istio’s request id as an attribute / response header so you can jump from Envoy logs to Tempo (`X-Trace-Id`). + +### Defaults (when env is unset) + +| Variable | Default here | +|----------|----------------| +| export / SDK | **disabled (no-op)** until `OTEL_EXPORTER_OTLP_ENDPOINT` (or traces endpoint) is set | +| `OTEL_SERVICE_NAME` | `cluster-utils-api` | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` (port **4318** on Alloy) | +| `OTEL_TRACE_SAMPLE_RATIO` | `1.0` (all traces when enabled) | +| `OTEL_TRACE_PROBES` | off (no spans for `/livez` `/readyz` `/startupz` `/metrics` `/ping`) | +| propagators | always **tracecontext + baggage + b3 + jaeger** | +| `OTEL_EXPORTER_OTLP_INSECURE` | unset (exporter default); set `"true"` in-cluster without TLS | + +On **every startup** we log a single line `otel config (effective)` with enabled flag, endpoints, protocol, sample ratio, probe tracing, and propagators — grep pod logs for `otel config`. + +### Enable traces (OTLP push) + +```yaml +env: + - name: OTEL_SERVICE_NAME + value: cluster-utils-api + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: "alloy.observability.svc.cluster.local:4318" # http/protobuf default + - name: OTEL_EXPORTER_OTLP_PROTOCOL + value: http/protobuf # or grpc (often :4317) + - name: OTEL_EXPORTER_OTLP_INSECURE + value: "true" # TLS off inside the mesh + # optional: + # - name: OTEL_TRACE_SAMPLE_RATIO + # value: "1.0" + # - name: OTEL_SDK_DISABLED + # value: "true" + # - name: OTEL_TRACE_PROBES + # value: "true" # also span kube probes (noisy) +``` + +What gets instrumented: + +- **Inbound HTTP** — Gin (`otelgin`) + mesh header attributes +- **Outbound `/a/proxy`** — `otelhttp` client span + header inject east-west +- **Response** `X-Trace-Id` (OTEL) and `X-Request-Id` (if the mesh/client sent one) + +```bash +curl -sS -D- -H 'X-Request-Id: demo-from-gateway' localhost:8080/debug -o /dev/null | grep -iE 'x-trace-id|x-request-id' +``` + +**Important:** `X-Request-Id` (Istio/Envoy) ≠ `X-Trace-Id` (OpenTelemetry/Tempo). +They are both useful; we keep both. In Tempo, search by trace id, or by span attribute `http.request_id` when the mesh sent a request id. Pod logs include `trace_id` + `request_id` on each request line when present. + +### Join Envoy / Istio access logs ↔ Tempo + +```bash +# 1) call through the mesh (or simulate Envoy's header) +curl -sS -D /tmp/hdrs -H 'X-Request-Id: 0a1b2c3d-demo' localhost:8080/debug -o /dev/null +grep -iE 'x-trace-id|x-request-id' /tmp/hdrs + +# 2) app logs (same ids) +# kubectl logs deploy/cluster-utils-api | grep 0a1b2c3d-demo + +# 3) Tempo: search TraceID = value of X-Trace-Id +# or attribute http.request_id = 0a1b2c3d-demo +``` + +On boot, always check: + +```bash +kubectl logs deploy/cluster-utils-api | grep 'otel config' +# → enabled, endpoint, protocol, sample_ratio, propagators, mesh_headers, … +``` + +### Alloy sketch + +```hcl +// metrics: scrape this app +prometheus.scrape "cu_api" { + targets = [{ __address__ = "cluster-utils-api-svc:8080" }] + metrics_path = "/metrics" + forward_to = [prometheus.remote_write.mimir.receiver] +} + +// traces: receive OTLP *push* from the app +otelcol.receiver.otlp "default" { + http { endpoint = "0.0.0.0:4318" } + grpc { endpoint = "0.0.0.0:4317" } + output { traces = [otelcol.exporter.otlp.tempo.input] } +} + +otelcol.exporter.otlp "tempo" { + client { endpoint = "tempo:4317" tls { insecure = true } } +} +``` + +Istio tip: prefer mesh config that emits **W3C** (or dual W3C+B3). If you only have B3 today, our B3 propagator still joins the chain. + +--- + ## Security This image is a **cluster debug tool**, not a public SaaS. Treat it like you treat `kubectl` access. diff --git a/RELEASE-v2.5.0.md b/RELEASE-v2.5.0.md new file mode 100644 index 0000000..ccd322e --- /dev/null +++ b/RELEASE-v2.5.0.md @@ -0,0 +1,64 @@ +## cluster-utils-api v2.5.0 — OpenTelemetry traces + +Adds OTLP **trace export** (push). Metrics are unchanged and still scraped. + +### Push vs scrape (read this twice) + +| Signal | How | Path | +|--------|-----|------| +| **Metrics** | **Scrape** (pull) | `GET /metrics` → Alloy/Prometheus → Mimir | +| **Traces** | **Push** | OTLP → Alloy → **Tempo** | + +Tempo does **not** scrape `/metrics` for spans. The app exports OTLP when configured. + +### Mesh / Istio — request id vs trace id + +This is the detail that usually bites people: + +- **`x-request-id`** (Envoy/Istio) is a **request correlation id** for access logs. +- **`traceparent` / OTEL trace id** (`X-Trace-Id` on our responses) is the **distributed trace** id in Tempo. +- They are **not the same value**. Both matter. + +**What we do:** + +- Propagate **W3C + B3 + Jaeger + baggage** so modern *and* Istio-era meshes join the same trace. +- Attach mesh ids as span attributes (`http.request_id`, correlation ids, …). +- Echo **`X-Trace-Id`** and **`X-Request-Id`** (when present) on responses. +- Put `trace_id` + `request_id` on JSON request logs so you can join Envoy logs ↔ Tempo. + +**How to join:** Tempo search by `X-Trace-Id`, or by attribute `http.request_id` = Envoy’s id. + +Other meshes (Linkerd, etc.) follow the same pattern: W3C is the normal default; B3 still shows up; product-specific headers are extra correlation. + +### Enable (defaults) + +Tracing is a **no-op** until an OTLP endpoint is set. + +| Variable | Default | +|----------|---------| +| export | off until `OTEL_EXPORTER_OTLP_ENDPOINT` (or traces endpoint) | +| protocol | `http/protobuf` (Alloy **:4318**) | +| service name | `cluster-utils-api` | +| sample ratio | `1.0` | +| probe spans | off (`OTEL_TRACE_PROBES=true` to include `/livez` etc.) | +| propagators | W3C + baggage + B3 + Jaeger | + +```yaml +OTEL_SERVICE_NAME=cluster-utils-api +OTEL_EXPORTER_OTLP_ENDPOINT=alloy.observability.svc:4318 +OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf +OTEL_EXPORTER_OTLP_INSECURE=true +``` + +On **every startup** logs include `otel config (effective)` — grep pod logs for `otel config` to see exactly what it started with. + +### What is instrumented + +- Inbound HTTP (Gin), with mesh header attributes +- Outbound `/a/proxy` (linked east-west spans + header inject) +- Probe/metrics/ping paths skipped unless `OTEL_TRACE_PROBES=true` + +### Images + +- `donkeyx/cluster-utils-api:2.5.0` / `:latest` +- `ghcr.io/donkeyx/cluster-utils-api:2.5.0` / `:latest` diff --git a/go.mod b/go.mod index 14dd787..fa5c434 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,16 @@ require ( github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/swag v1.16.6 + go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.70.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 + go.opentelemetry.io/contrib/propagators/b3 v1.45.0 + go.opentelemetry.io/contrib/propagators/jaeger v1.45.0 + go.opentelemetry.io/otel v1.45.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 + go.opentelemetry.io/otel/sdk v1.45.0 + go.opentelemetry.io/otel/trace v1.45.0 go.uber.org/zap v1.28.0 ) @@ -17,44 +27,53 @@ require ( github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/beorn7/perks v1.0.1 // 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/bytedance/gopkg v0.1.4 // indirect + github.com/bytedance/sonic v1.15.2 // indirect + github.com/bytedance/sonic/loader v0.5.2 // 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/cloudwego/base64x v0.1.7 // 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/felixge/httpsnoop v1.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.15 // indirect + github.com/gin-contrib/sse v1.1.1 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/spec v0.20.4 // indirect github.com/go-openapi/swag v0.19.15 // 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/go-playground/validator/v10 v10.30.3 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/josharian/intern v1.0.0 // 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/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/leodido/go-urn v1.5.0 // indirect github.com/mailru/easyjson v0.7.6 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.24 // 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/pelletier/go-toml/v2 v2.4.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.70.1 // indirect github.com/prometheus/procfs v0.21.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect - github.com/quic-go/quic-go v0.59.0 // indirect + github.com/quic-go/quic-go v0.61.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect - go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.mongodb.org/mongo-driver/v2 v2.8.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/metric v1.45.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/arch v0.22.0 // indirect + golang.org/x/arch v0.29.0 // indirect golang.org/x/crypto v0.54.0 // indirect golang.org/x/mod v0.37.0 // indirect golang.org/x/net v0.57.0 // indirect @@ -62,6 +81,9 @@ require ( golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.40.0 // indirect golang.org/x/tools v0.47.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect + google.golang.org/grpc v1.83.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 83d9ca8..1678ea2 100644 --- a/go.sum +++ b/go.sum @@ -6,28 +6,37 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 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.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/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= +github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo= +github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA= +github.com/bytedance/sonic/loader v0.5.2 h1:0QtP1gevc1OZ6/H8Lb9BRZiCXd1Ftjd3OKuj1T1lBIo= +github.com/bytedance/sonic/loader v0.5.2/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/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI= +github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= -github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= +github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI= +github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= -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-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko= +github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s= 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.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/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-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -44,23 +53,29 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o 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/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/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8= +github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/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/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/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -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/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -70,14 +85,14 @@ 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/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0= +github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -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/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= 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= @@ -86,8 +101,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -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/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/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.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= @@ -98,12 +113,14 @@ github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= +github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= +github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= 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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/quic-go/quic-go v0.61.0 h1:ui88A53s8MSVYLC56en0KQ17HARk+9986Dn0SBfKNvA= +github.com/quic-go/quic-go v0.61.0/go.mod h1:9So2anK4Tp22URSQq00k+Vo2PNkle96ycDPDHL4s9vs= +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= @@ -127,8 +144,38 @@ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2 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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -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.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8= +go.mongodb.org/mongo-driver/v2 v2.8.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/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.70.0 h1:R+uYJnPiZLeJhFicamvZhLr0aVOrDIaxBcqgGus9nSU= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.70.0/go.mod h1:Zwk515MbVWCK2WOgeYBNIf8CyZGbAgkoJ6VKSGkd6aQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04= +go.opentelemetry.io/contrib/propagators/b3 v1.45.0 h1:audI5r8RmWVSORhzA5Y57yGvEA1358PvGk0u0sMOTDA= +go.opentelemetry.io/contrib/propagators/b3 v1.45.0/go.mod h1:SiENIek0FnzLni3/jSCiumyCA2mwP8uGaE1686SOJug= +go.opentelemetry.io/contrib/propagators/jaeger v1.45.0 h1:e8U4utKt9oV2TfLKZFqUzz5shYKnUf3DISalTpLs4lA= +go.opentelemetry.io/contrib/propagators/jaeger v1.45.0/go.mod h1:lx91c/ZlmgS2rjGOuXB+Mmq+f0QxzC9UjYUuJwR4tvQ= +go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= +go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 h1:QBajQ2SrwQijzHyZbQlPsuIzpl/ll8DY6wPWsajeGcI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0/go.mod h1:08ZQLjrPLQ6R4kAXvuOvODEer5Yh4CoFvll5qB2BCI8= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.45.0 h1:lsA/S1bxgdbyFGkTj+3meEdJ6ADVU7QoFstV6MXgE68= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.45.0/go.mod h1:L7u+MirGoB1bjeLH66+xDykF4RC8C3RN7lIFpBiewUo= +go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= +go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= +go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= +go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= +go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= +go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= +go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= +go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= 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= @@ -141,8 +188,8 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= -golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/arch v0.29.0 h1:8sSET5wB0+exBm0FGmOtdHMqjlRdV2DRD3/IV6OZgho= +golang.org/x/arch v0.29.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= @@ -168,7 +215,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -187,6 +233,14 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc= +google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= +google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= 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= diff --git a/handlers/proxy.go b/handlers/proxy.go index 0c26ba1..c144749 100644 --- a/handlers/proxy.go +++ b/handlers/proxy.go @@ -10,6 +10,7 @@ import ( "time" "github.com/gin-gonic/gin" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) // hop-by-hop headers we never copy outbound @@ -171,7 +172,11 @@ func ProxyHandler(c *gin.Context) { hostname, _ := os.Hostname() outReq.Header.Add("X-Cu-Proxy-Hop", hostname) - client := &http.Client{Timeout: time.Duration(timeout * float64(time.Second))} + // otelhttp propagates W3C trace context east-west and creates a client span + client := &http.Client{ + Timeout: time.Duration(timeout * float64(time.Second)), + Transport: otelhttp.NewTransport(http.DefaultTransport), + } start := time.Now() resp, err := client.Do(outReq) elapsed := time.Since(start) diff --git a/k8s-cluster-util-apis.yml b/k8s-cluster-util-apis.yml index 8609099..b1542de 100644 --- a/k8s-cluster-util-apis.yml +++ b/k8s-cluster-util-apis.yml @@ -77,6 +77,18 @@ spec: # value: "10" # - name: AUTH_TOKEN # value: "fixed-token-for-tests" + # --- OTEL traces (OTLP *push* to Alloy → Tempo; not scraped) --- + # - name: OTEL_SERVICE_NAME + # value: cluster-utils-api + # - name: OTEL_EXPORTER_OTLP_ENDPOINT + # value: alloy.observability.svc.cluster.local:4318 + # - name: OTEL_EXPORTER_OTLP_PROTOCOL + # value: http/protobuf + # - name: OTEL_EXPORTER_OTLP_INSECURE + # value: "true" + # include kube probe paths in traces (off by default — noisy) + # - name: OTEL_TRACE_PROBES + # value: "true" resources: requests: cpu: "0.1" diff --git a/main.go b/main.go index 7533c53..d542613 100644 --- a/main.go +++ b/main.go @@ -10,16 +10,23 @@ package main import ( + "context" "fmt" "math/rand" "os" "strconv" + "time" "github.com/donkeyx/cluster-utils-api/handlers" "github.com/donkeyx/cluster-utils-api/middleware" + "github.com/donkeyx/cluster-utils-api/otelsetup" "github.com/donkeyx/cluster-utils-api/routes" + "net/http" + "github.com/gin-gonic/gin" + "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" + "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -39,8 +46,26 @@ func main() { handlers.SetBuildInfo(Version, GitHash) handlers.InitProbesFromEnv() + // Traces: OTLP *push* to Alloy/collector (not scraped). No-op if endpoint unset. + ctx := context.Background() + otelShutdown, err := otelsetup.Init(ctx, "cluster-utils-api", Version, logger) + if err != nil { + logger.Fatal("otel init failed", zap.Error(err)) + } + defer func() { + shCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := otelShutdown(shCtx); err != nil { + logger.Warn("otel shutdown", zap.Error(err)) + } + }() + gin.SetMode(gin.ReleaseMode) r := gin.New() + // Order: create span → mesh attrs + echo ids → metrics → logs → recover + r.Use(otelgin.Middleware("cluster-utils-api", otelgin.WithFilter(otelPathFilter))) + r.Use(traceAndRequestIDHeaders()) + r.Use(handlers.MetricsMiddleware()) r.Use(middleware.LoggerMiddleware(logger)) r.Use(gin.Recovery()) @@ -106,3 +131,40 @@ func getEnvOrDefault(key string, defaultValue int) int { } return defaultValue } + +// otelPathFilter skips ultra-noisy probes/metrics by default so Tempo isn't flooded. +// Set OTEL_TRACE_PROBES=true to include them when you're debugging probe behaviour itself. +func otelPathFilter(r *http.Request) bool { + if os.Getenv("OTEL_TRACE_PROBES") == "true" { + return true + } + switch r.URL.Path { + case "/livez", "/readyz", "/startupz", + "/health", "/healthz", "/ready", "/startup", + "/metrics", "/ping": + return false + default: + return true + } +} + +// traceAndRequestIDHeaders: +// - attaches Istio/Envoy x-request-id (and friends) onto the span +// - echoes X-Trace-Id (OTEL) and X-Request-Id (mesh) on the response for easy curl ↔ Tempo/access-log joins +func traceAndRequestIDHeaders() gin.HandlerFunc { + return func(c *gin.Context) { + reqID := otelsetup.AnnotateMeshHeaders(c.Request.Context(), c.GetHeader) + c.Next() + + sc := trace.SpanFromContext(c.Request.Context()).SpanContext() + if sc.IsValid() { + c.Writer.Header().Set("X-Trace-Id", sc.TraceID().String()) + } + // Prefer inbound mesh id; otherwise leave empty (don't invent — Envoy owns that space) + if reqID != "" { + c.Writer.Header().Set("X-Request-Id", reqID) + } else if inbound := c.GetHeader("X-Request-Id"); inbound != "" { + c.Writer.Header().Set("X-Request-Id", inbound) + } + } +} diff --git a/main_test.go b/main_test.go index 3366824..3006a68 100644 --- a/main_test.go +++ b/main_test.go @@ -23,6 +23,8 @@ func setupTestRouter(token string) *gin.Engine { // reset to known defaults for tests (env may not be set) resetProbesForTest() r := gin.New() + // same order as main (otel no-op without endpoint) + r.Use(handlers.MetricsMiddleware()) logger := setupLogger() routes.SetupRouter(logger, token, r) return r @@ -308,6 +310,7 @@ func TestProxyHopToSelf(t *testing.T) { handlers.InitProbesFromEnv() resetProbesForTest() r := gin.New() + r.Use(handlers.MetricsMiddleware()) logger := setupLogger() routes.SetupRouter(logger, token, r) diff --git a/middleware/middleware.go b/middleware/middleware.go index caa9e1d..ffa209f 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gin-gonic/gin" + "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) @@ -15,18 +16,31 @@ func LoggerMiddleware(logger *zap.Logger) gin.HandlerFunc { // Process the request c.Next() - // Log request and response details - end := time.Now() - latency := end.Sub(start) + // Process the request + latency := time.Since(start) - logger.Info("Request", + fields := []zap.Field{ zap.String("method", c.Request.Method), zap.String("path", c.Request.URL.Path), zap.String("ip", c.ClientIP()), zap.String("user_agent", c.Request.UserAgent()), zap.Int("status", c.Writer.Status()), zap.Duration("latency", latency), - ) + } + // join mesh access logs ↔ Tempo + if rid := c.Writer.Header().Get("X-Request-Id"); rid != "" { + fields = append(fields, zap.String("request_id", rid)) + } else if rid := c.GetHeader("X-Request-Id"); rid != "" { + fields = append(fields, zap.String("request_id", rid)) + } + if sc := trace.SpanFromContext(c.Request.Context()).SpanContext(); sc.IsValid() { + fields = append(fields, + zap.String("trace_id", sc.TraceID().String()), + zap.String("span_id", sc.SpanID().String()), + ) + } + + logger.Info("Request", fields...) } } diff --git a/otelsetup/otel.go b/otelsetup/otel.go new file mode 100644 index 0000000..1baaf39 --- /dev/null +++ b/otelsetup/otel.go @@ -0,0 +1,304 @@ +// Package otelsetup wires OpenTelemetry traces (OTLP push — not scraped). +// +// Traces are pushed to a collector (Grafana Alloy, OTel Collector, etc.) via OTLP. +// Metrics stay on GET /metrics (Prometheus scrape). Different pipelines. +// +// End-to-end mesh tracing (Istio / Linkerd / etc.): +// +// - W3C traceparent/tracestate — modern default (Envoy, OTEL, most stacks) +// - B3 (x-b3-*) — still very common with Istio/Zipkin-style configs +// - Jaeger uber-trace-id — older but still around +// - x-request-id — Envoy/Istio correlation id (NOT a trace context header); +// we attach it as a span attribute + echo on responses so you can join +// access logs ↔ Tempo (same idea as x-correlation-id, x-amzn-trace-id, …) +// +// Standard env (and what we default when unset): +// +// OTEL_SDK_DISABLED default false (but no-op until endpoint set) +// OTEL_SERVICE_NAME default cluster-utils-api (or Init arg) +// OTEL_SERVICE_VERSION from binary Version when Init is called +// OTEL_EXPORTER_OTLP_ENDPOINT required to enable export +// OTEL_EXPORTER_OTLP_TRACES_ENDPOINT optional override +// OTEL_EXPORTER_OTLP_PROTOCOL default http/protobuf (:4318) +// OTEL_EXPORTER_OTLP_INSECURE often true in-cluster (exporter reads env) +// OTEL_TRACES_EXPORTER none disables; else otlp when endpoint set +// OTEL_TRACE_SAMPLE_RATIO default 1.0 +// OTEL_TRACE_PROBES default false (skip /livez etc.) +// OTEL_PROPAGATORS optional; we always install W3C+B3+Jaeger+baggage +// +// If no OTLP endpoint is set, tracing is a no-op so local runs stay quiet. +package otelsetup + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + "go.opentelemetry.io/contrib/propagators/b3" + "go.opentelemetry.io/contrib/propagators/jaeger" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.37.0" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" +) + +// Init sets the global TracerProvider + multi-format propagators for mesh interop. +// Returns a shutdown func (flush spans). Safe when tracing is disabled (noop). +func Init(ctx context.Context, serviceName, serviceVersion string, log *zap.Logger) (shutdown func(context.Context) error, err error) { + if serviceName == "" { + serviceName = "cluster-utils-api" + } + if v := strings.TrimSpace(os.Getenv("OTEL_SERVICE_NAME")); v != "" { + serviceName = v + } + + noop := func(context.Context) error { return nil } + + // Always install propagators so inbound W3C/B3 still extract into context + // even when we are not exporting (helps local middleware / future hops). + installPropagators() + + cfg := effectiveConfig(serviceName, serviceVersion) + logConfig(log, cfg) + + if !cfg.Enabled { + return noop, nil + } + + res, err := resource.New(ctx, + resource.WithFromEnv(), // OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_* + resource.WithProcess(), + resource.WithTelemetrySDK(), + resource.WithHost(), + resource.WithAttributes( + semconv.ServiceName(cfg.ServiceName), + semconv.ServiceVersion(cfg.ServiceVersion), + ), + ) + if err != nil { + return nil, fmt.Errorf("otel resource: %w", err) + } + + exp, err := newTraceExporter(ctx, cfg.Protocol) + if err != nil { + return nil, fmt.Errorf("otel exporter: %w", err) + } + + tp := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exp, + sdktrace.WithMaxExportBatchSize(512), + sdktrace.WithBatchTimeout(5*time.Second), + ), + sdktrace.WithResource(res), + sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(cfg.SampleRatio))), + ) + + otel.SetTracerProvider(tp) + + log.Info("otel traces enabled (OTLP push — not scrape)", + zap.String("service", cfg.ServiceName), + zap.String("version", cfg.ServiceVersion), + zap.String("protocol", cfg.Protocol), + zap.String("endpoint", cfg.Endpoint), + zap.Float64("sample_ratio", cfg.SampleRatio), + zap.Strings("propagators", cfg.Propagators), + ) + + return tp.Shutdown, nil +} + +// Config is the resolved startup picture (for logs / tests). +type Config struct { + Enabled bool + DisabledReason string + ServiceName string + ServiceVersion string + Endpoint string + Protocol string + InsecureEnv string + SampleRatio float64 + TraceProbes bool + Propagators []string +} + +func effectiveConfig(serviceName, serviceVersion string) Config { + cfg := Config{ + ServiceName: serviceName, + ServiceVersion: serviceVersion, + Endpoint: endpointForLog(), + Protocol: protocol(), + InsecureEnv: firstEnv("OTEL_EXPORTER_OTLP_INSECURE", "OTEL_EXPORTER_OTLP_TRACES_INSECURE"), + SampleRatio: sampleRatio(), + TraceProbes: os.Getenv("OTEL_TRACE_PROBES") == "true", + Propagators: []string{"tracecontext", "baggage", "b3", "jaeger"}, + } + + switch { + case strings.EqualFold(os.Getenv("OTEL_SDK_DISABLED"), "true"): + cfg.DisabledReason = "OTEL_SDK_DISABLED=true" + case strings.EqualFold(os.Getenv("OTEL_TRACES_EXPORTER"), "none"): + cfg.DisabledReason = "OTEL_TRACES_EXPORTER=none" + case !endpointConfigured(): + cfg.DisabledReason = "no OTEL_EXPORTER_OTLP_ENDPOINT (or _TRACES_ENDPOINT)" + default: + cfg.Enabled = true + } + return cfg +} + +func logConfig(log *zap.Logger, cfg Config) { + // One greppable block so "what did it start with?" is obvious in pod logs. + log.Info("otel config (effective)", + zap.Bool("enabled", cfg.Enabled), + zap.String("disabled_reason", cfg.DisabledReason), + zap.String("OTEL_SERVICE_NAME", cfg.ServiceName), + zap.String("service_version", cfg.ServiceVersion), + zap.String("OTEL_EXPORTER_OTLP_ENDPOINT", envOr("(unset)", "OTEL_EXPORTER_OTLP_ENDPOINT")), + zap.String("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", envOr("(unset)", "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")), + zap.String("OTEL_EXPORTER_OTLP_PROTOCOL", cfg.Protocol+" (default http/protobuf if unset)"), + zap.String("OTEL_EXPORTER_OTLP_INSECURE", envOr("(unset)", "OTEL_EXPORTER_OTLP_INSECURE", "OTEL_EXPORTER_OTLP_TRACES_INSECURE")), + zap.String("OTEL_SDK_DISABLED", envOr("false", "OTEL_SDK_DISABLED")), + zap.String("OTEL_TRACES_EXPORTER", envOr("(auto)", "OTEL_TRACES_EXPORTER")), + zap.Float64("OTEL_TRACE_SAMPLE_RATIO", cfg.SampleRatio), + zap.Bool("OTEL_TRACE_PROBES", cfg.TraceProbes), + zap.Strings("propagators", cfg.Propagators), + zap.String("mesh_headers", "traceparent, x-b3-*, uber-trace-id, x-request-id (attr)"), + ) +} + +func installPropagators() { + // Extract/inject in priority order: first match wins on extract for some fields. + // Composite tries all; W3C first is the modern default, B3/Jaeger for Istio-era traffic. + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + b3.New(b3.WithInjectEncoding(b3.B3MultipleHeader|b3.B3SingleHeader)), + jaeger.Jaeger{}, + )) +} + +// AnnotateMeshHeaders copies common mesh / gateway correlation headers onto the active span +// and returns the request id (if any) for response headers. +func AnnotateMeshHeaders(ctx context.Context, header func(string) string) (requestID string) { + span := trace.SpanFromContext(ctx) + if !span.IsRecording() { + // still surface request id for response even without export + return firstNonEmpty( + header("X-Request-Id"), + header("X-Request-ID"), + header("X-Correlation-Id"), + header("X-Correlation-ID"), + ) + } + + var attrs []attribute.KeyValue + add := func(hdr, key string) { + if v := strings.TrimSpace(header(hdr)); v != "" { + attrs = append(attrs, attribute.String(key, v)) + } + } + + // Envoy / Istio request id (access log correlation — not the same as OTEL trace id) + for _, h := range []string{"X-Request-Id", "X-Request-ID"} { + if v := strings.TrimSpace(header(h)); v != "" { + attrs = append(attrs, attribute.String("http.request_id", v)) + requestID = v + break + } + } + for _, h := range []string{"X-Correlation-Id", "X-Correlation-ID"} { + if v := strings.TrimSpace(header(h)); v != "" { + attrs = append(attrs, attribute.String("http.correlation_id", v)) + if requestID == "" { + requestID = v + } + break + } + } + + // Cloud / product variants (attach if present; propagation may already handle some) + add("X-Amzn-Trace-Id", "aws.xray.trace_id") + add("X-Cloud-Trace-Context", "gcp.cloud_trace_context") + add("X-B3-TraceId", "messaging.b3.trace_id") // also in B3 propagator; useful as attr for search + + if len(attrs) > 0 { + span.SetAttributes(attrs...) + } + return requestID +} + +func endpointConfigured() bool { + return strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")) != "" || + strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")) != "" +} + +func endpointForLog() string { + if v := os.Getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"); v != "" { + return v + } + return os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") +} + +func protocol() string { + proto := strings.ToLower(strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL"))) + if proto == "" { + proto = strings.ToLower(strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"))) + } + if proto == "" { + return "http/protobuf" + } + if strings.Contains(proto, "grpc") { + return "grpc" + } + return "http/protobuf" +} + +func sampleRatio() float64 { + v := strings.TrimSpace(os.Getenv("OTEL_TRACE_SAMPLE_RATIO")) + if v == "" { + return 1.0 + } + var f float64 + if _, err := fmt.Sscanf(v, "%f", &f); err != nil || f < 0 || f > 1 { + return 1.0 + } + return f +} + +func newTraceExporter(ctx context.Context, proto string) (*otlptrace.Exporter, error) { + if proto == "grpc" { + return otlptracegrpc.New(ctx) + } + return otlptracehttp.New(ctx) +} + +func envOr(fallback string, keys ...string) string { + for _, k := range keys { + if v, ok := os.LookupEnv(k); ok && strings.TrimSpace(v) != "" { + return v + } + } + return fallback +} + +func firstEnv(keys ...string) string { + return envOr("", keys...) +} + +func firstNonEmpty(vals ...string) string { + for _, v := range vals { + if strings.TrimSpace(v) != "" { + return strings.TrimSpace(v) + } + } + return "" +} diff --git a/routes/routes.go b/routes/routes.go index b3ad33d..b83ac6d 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -19,7 +19,7 @@ import ( var swaggerInfoMu sync.Mutex func SetupRouter(logger *zap.Logger, st string, r *gin.Engine) { - r.Use(handlers.MetricsMiddleware()) + // Middleware (otel / metrics / log / recover) is registered in main before this. // Redirect to swagger docs r.GET("/", func(c *gin.Context) {