OctoWiki

Metrics & Tracing

The OpenTelemetry → SigNoz observability catalog — every custom metric with its type/unit/attributes, the tracing conventions, log fields, the export pipeline, and the dimension-compliance gaps and dead metrics.

Full observability catalog: OpenTelemetry SDK → OTLP/HTTP → SigNoz (traces + metrics + logs share one endpoint/token). OTLP push only — there is no Prometheus scrape endpoint. For the operational view see Jobs & Observability; this is the signal catalog.

Init (main.go:127, gated on OTEL_ENABLED): InitTracerInitMetricsExportermetrics.InitInstruments()InitLogExporter. Env vars in Environment → Telemetry.

Metrics inventory

Every metric name is prefixed with SERVICE_NAME (prefix(), instruments.go:15), so the real name is e.g. octopus.business.orders.created. Attributes are applied at record time.

HTTP / infra (middleware/metrics.go)

MetricTypeUnitAttributes
http.server.requestsInt64Counter{request}route_group, method, status_code, route
http.server.request.durationFloat64Histogramsroute_group, method, status_code, route
http.server.active_requestsInt64UpDownCounter{request}route_group

route_group ∈ api / admin / client / auth / webhook / other; route = registered pattern (not raw URL).

Business metrics (business.*)

MetricTypeAttributesRecorded at
business.orders.createdInt64Countervoucher: type/status/sub_status (repo/order.go:735); topup/esim: +vendor_id/client_id/product_id/currency3 sites
business.orders.amountFloat64Histogramtopup/esim: type/status/vendor_id/client_id/product_id/currency2 sites
business.orders.itemsInt64Histogramtopup/esim same set2 sites
business.vouchers.issuedInt64Countervendor_id, order_id (no product_id)create_voucher_order.go:3006
business.vouchers.failedInt64Counter💀 never recorded
business.topups.processedInt64Countertype/status/vendor_id/client_id/product_id/currencytopup_order.go:281
business.topups.amountFloat64Histogramsametopup_order.go:282
business.payouts.createdInt64Counterstatus, client_id, currencypayout_service.go:184/200
business.payouts.amountFloat64Histogramstatus, client_id, currencypayout_service.go:201
business.wallet.transactionsInt64Countertype, client_id, currency_idrepo/wallet.go:483
business.wallet.transaction_amountFloat64Histogramtype, client_id, currency_idrepo/wallet.go:484
business.vendor_api.requestsInt64Counter💀 never recorded
business.vendor_api.latencyFloat64Histogram💀 never recorded
business.inventory.allocatedInt64Counterproduct_id (no vendor/client_id)inventory.go:824
business.inventory.depletedInt64Counterproduct_idinventory.go:831
business.g2a.reservations_createdInt64Counterclient_idg2a_reservation.go:165
business.g2a.orders_createdInt64Counterclient_id, statusg2a_order.go:237
business.g2a.codes_deliveredInt64Counterclient_id, product_id ✅g2a_order.go:240
business.g2a.notifications_receivedInt64Counterclient_id, typeg2a.go:188

The metrics-dimensions rule is only partly honored

The rule requires business metrics to carry client_id + product_id for Grafana/SigNoz drill-down. Compliance:

  • ✅ Full: topups.*, orders.* (topup/esim paths), g2a.codes_delivered.
  • ⚠️ client_id only: payouts.* (no product concept), wallet.*, g2a.reservations_created/orders_created/notifications_received.
  • ❌ product_id only, no client_id: inventory.allocated/depleted.
  • neither: orders.created on the voucher/repo path (type/status/sub_status only), vouchers.issued (vendor_id/order_id only).

So voucher order creation — the flagship KPI — records without client_id or product_id, and does not record amount/items at all (only the topup/esim paths do).

Non-OTel metrics (in-process, not exported)

metrics/metrics.go has two in-memory trackers surfaced only via the admin dashboard API — never pushed to OTel or Prometheus:

  • ServerMetrics — uptime %, avg API response time (fed by RecordAPIResponse from the tracing middleware).
  • CronMetrics — per-job LastRun/NextRun/Duration/Status, persisted to Redis cron:metrics:jobs (24h TTL). This is the "cron Prometheus metrics" the scheduler mentions — but it's Go structs + Redis, not Prometheus. See Cron Catalog.

Tracing

utils/tracer.go InitTracer:

  • Exporter: otlptracehttp to OTEL_EXPORTER_OTLP_ENDPOINT (token header signoz-access-token; TLS off when INSECURE_MODE=true).
  • Sampler: AlwaysSample()100% sampling.
  • Propagation: W3C TraceContext + Baggage — so traces continue across services (e.g. Grasshopper → backend via traceparent).
  • Global utils.Tracer defaults to a noop tracer so uninitialized paths don't nil-panic.

Span convention — manual, no generic wrapper: ctx, span := utils.Tracer.Start(ctx, "<name>"); defer span.End(). ~1480 call sites. Names are layer.operation, kebab-case op:

PrefixSitesLayer
repo.*~780repository/DB (dominant)
handler.*~223HTTP handlers
service.* / services.* / inventory-service / prefetch-service~40service layer (inconsistent prefix)
admin.*, admin-api.*, webhook.*, shopify.*, webauthn.*domain handlers
*-job, processor, execution-trackerscheduler/jobs

HTTP entry span comes from otelfiber.Middleware() (the request-root span); the custom TracingMiddleware deliberately creates no extra span — it reads the otelfiber span's IDs and enriches the logger.

Span attributes are set explicitly in scheduler tasks (task_id, client_count) but there is no generic tenant_id/client_id span attribute injected in middleware. Tenant/client context rides in metric labels and log fields, not span attrs.

Logs

Zap (middleware/logging.go). Logger(ctx) pulls a request-enriched *zap.SugaredLogger from context, else builds a default JSON logger (ISO8601 timestamp, lowercase level, msg, caller; initial fields pid + service_name).

Per-request fields (TracingMiddleware): trace_id, span_id, method, path, ip. Request ID = X-Request-Id or trace_id fallback (echoed back). Debug dumps mask sensitive headers (authorization/cookie/x-api-key/…) and truncate the body to 4096 bytes; the completion log Request completed carries status/duration_ms/response_size.

OTel log export (utils/log_exporter.go): otlploghttp to the same endpoint, bridged via otelzap.NewCore teed with stdout — logs go to both stdout and SigNoz. Known cosmetic caveat: otelzap can't read trace/span IDs from Go ctx, so they're duplicated as Zap fields (correlation still works). LOG_LEVEL gates debug.

Export pipeline

ConcernValue
Master switchOTEL_ENABLED (default false, fatal if unparseable)
EndpointOTEL_EXPORTER_OTLP_ENDPOINT (default localhost:4318)
AuthOTEL_EXPORTER_OTLP_TOKENsignoz-access-token header
TLSINSECURE_MODE (default true → no TLS)
Metric intervalOTEL_METRIC_EXPORT_INTERVAL (default 60s, floor 10s), PeriodicReader
Resourceservice.name=SERVICE_NAME; service.version=SERVICE_VERSION (metrics/logs only); environment+deployment.environment=ENVIRONMENT

There is no SIGNOZ_ENDPOINT var — SigNoz is reached via the OTLP endpoint+token. See Infrastructure → telemetry (SigNoz UI on the sandbox box).

No committed dashboards — no Grafana/SigNoz dashboard JSON in-repo (only the signoz.conf reverse proxy + a health-check script). The admin "dashboard" is an application feature (ServerMetrics/CronMetrics), not an observability dashboard. Drill-down dimensions are enforced only by convention at record sites (see the gaps above).

Findings

CategoryDetail
Dead metricsvouchers.failed, vendor_api.requests, vendor_api.latency — defined, never recorded
Missing dimsvoucher orders.created (no client_id/product_id), vouchers.issued, inventory.* (no client_id), several g2a.* (no product_id)
Doc-vs-actualinstrument struct comments wrong: vouchers.issued doc says vendor,product_id (actual vendor_id,order_id); wallet.* doc type,source (actual type,client_id,currency_id); orders.* source label never emitted
Naming inconsistencyspan prefix service. vs services. vs inventory-service.; vendor_id vs vendor; currency vs currency_id; trace resource omits service.version; stale "Mimir" comment (main.go:135) though backend is SigNoz

Key files

  • Metrics: metrics/instruments.go, metrics/metrics.go, middleware/metrics.go
  • Tracing: utils/tracer.go, middleware/tracing.go
  • Logs: middleware/logging.go, utils/log_exporter.go
  • Export: utils/metrics_exporter.go; wiring main.go:120-148,490-499

On this page