Observability

May 12, 2026 · View on GitHub

The ToolHive Registry Server provides comprehensive observability through OpenTelemetry (OTEL), supporting both distributed tracing and metrics collection via OTLP exporters.

Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                     Registry Server                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐                 │
│  │   HTTP      │  │   Sync      │  │  Registry   │                 │
│  │ Middleware  │  │  Metrics    │  │  Metrics    │                 │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘                 │
│         │                │                │                         │
│         └────────────────┼────────────────┘                         │
│                          │                                          │
│                   ┌──────▼──────┐                                   │
│                   │  Telemetry  │                                   │
│                   │   Facade    │                                   │
│                   └──────┬──────┘                                   │
│                          │                                          │
│         ┌────────────────┼────────────────┐                         │
│         │                │                │                         │
│  ┌──────▼──────┐  ┌──────▼──────┐  ┌──────▼──────┐                 │
│  │   Tracer    │  │    Meter    │  │   Resource  │                 │
│  │  Provider   │  │  Provider   │  │  Attributes │                 │
│  └──────┬──────┘  └──────┬──────┘  └─────────────┘                 │
│         │                │                                          │
│         └────────┬───────┘                                          │
│                  │ OTLP HTTP                                        │
└──────────────────┼──────────────────────────────────────────────────┘


          ┌────────────────┐
          │      OTEL      │
          │   Collector    │
          └───────┬────────┘

        ┌─────────┼─────────┐
        │         │         │
        ▼         ▼         ▼
   ┌────────┐ ┌────────┐ ┌────────┐
   │ Jaeger │ │Promethe│ │ Grafana│
   │        │ │   us   │ │        │
   └────────┘ └────────┘ └────────┘

Package Structure

The telemetry implementation is located in internal/telemetry/:

FileResponsibility
telemetry.goMain facade orchestrating tracer and meter providers
config.goConfiguration types with validation and defaults
tracer.goTracerProvider setup with OTLP HTTP exporter
meter.goMeterProvider setup with OTLP HTTP exporter
metrics.goApplication-specific metrics (registry and sync)
middleware.goHTTP metrics middleware for Chi router
tracing_middleware.goHTTP tracing middleware for distributed tracing

Configuration

Telemetry is configured via the main application config file:

telemetry:
  enabled: true
  serviceName: "thv-registry-api"
  serviceVersion: "1.0.0"
  endpoint: "otel-collector:4318"
  insecure: true
  tracing:
    enabled: true
    sampling: 0.05  # 5% of traces sampled
  metrics:
    enabled: true

Configuration Options

OptionTypeDefaultDescription
enabledboolfalseEnable/disable all telemetry
serviceNamestring"thv-registry-api"Service name in telemetry data
serviceVersionstring""Service version in telemetry data
endpointstring"localhost:4318"OTLP HTTP endpoint
insecureboolfalseUse insecure connection (no TLS)
tracing.enabledboolfalseEnable distributed tracing
tracing.samplingfloat640.05Trace sampling ratio (0.0-1.0)
metrics.enabledboolfalseEnable metrics collection

Metrics Reference

All metrics are prefixed with thv_reg_srv_ to distinguish them from other metrics in the system.

MetricTypeLabelsDescription
thv_reg_srv_http_request_duration_secondsHistogrammethod, route, status_codeDuration of HTTP requests
thv_reg_srv_http_requests_totalCountermethod, route, status_codeTotal number of HTTP requests
thv_reg_srv_http_active_requestsUpDownCounter-Number of in-flight requests
thv_reg_srv_servers_totalGaugesourceNumber of distinct servers in each source
thv_reg_srv_skills_totalGaugesourceNumber of distinct skills in each source
thv_reg_srv_sync_duration_secondsHistogramregistry, successDuration of sync operations

Histogram Buckets

  • HTTP metrics: 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 seconds
  • Sync metrics: 0.1, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300 seconds

Distributed Tracing

The Registry Server implements distributed tracing across three layers: HTTP, Service, and Sync operations. Traces provide end-to-end visibility into request flows and background operations.

Trace Hierarchy

┌─────────────────────────────────────────────────────────────────────┐
│ HTTP Request Span (root)                                            │
│ Name: "GET /registry/v0.1/servers"                                  │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Service Span (child)                                            │ │
│ │ Name: "dbService.ListServers"                                   │ │
│ │  - Attributes: registry.name, pagination.limit, result.count    │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│ Background Sync Span                                                │
│ Name: "sync.performRegistrySync"                                    │
│  - Attributes: registry.name, registry.type, sync.success,         │
│                sync.duration_seconds, sync.server_count             │
└─────────────────────────────────────────────────────────────────────┘

Span Reference

HTTP Layer Spans

Span NameKindDescription
{METHOD} {route}ServerRoot span for all HTTP requests

Attributes:

AttributeTypeDescription
http.request.methodstringHTTP method (GET, POST, etc.)
http.routestringRoute pattern (e.g., /registry/v0.1/servers/{serverName})
url.pathstringActual URL path
user_agent.originalstringClient user agent
http.response.status_codeintResponse status code

Excluded Endpoints:

The following endpoints are intentionally excluded from tracing:

EndpointReason for Exclusion
/healthHealth check endpoint
/readinessReadiness probe endpoint

Rationale for excluding health and readiness endpoints:

  1. High frequency, low diagnostic value: Health and readiness probes are typically called every 5-30 seconds by Kubernetes or load balancers. This generates a high volume of nearly identical spans that provide minimal insight into application behavior.

  2. Trace storage costs: Each span consumes storage in your tracing backend (e.g., Jaeger, Tempo). Health check spans can account for 50-90% of total span volume while providing almost no diagnostic value, significantly increasing storage costs.

  3. Signal-to-noise ratio: When investigating issues, health check spans clutter trace views and make it harder to find meaningful application traces. Excluding them improves the signal-to-noise ratio for debugging.

  4. Predictable behavior: Health and readiness endpoints have simple, predictable behavior (return 200 OK or error). Unlike business logic endpoints, they rarely need trace-level debugging—HTTP metrics are sufficient for monitoring their behavior.

  5. Industry best practice: Most observability frameworks and guidelines recommend excluding infrastructure endpoints from tracing. The OpenTelemetry community generally advises filtering out health checks at the instrumentation level.

If you need to debug health check issues, HTTP metrics (thv_reg_srv_http_request_duration_seconds) still capture latency and error rates for these endpoints.

Service Layer Spans

Span NameDescription
dbService.ListServersList servers with optional filtering
dbService.ListServerVersionsList versions of a specific server
dbService.GetServerVersionGet a specific server version
dbService.PublishServerVersionPublish a new server version
dbService.DeleteServerVersionDelete a server version
dbService.ListRegistriesList all registries
dbService.GetRegistryByNameGet a specific registry
dbService.CreateRegistryCreate a new registry
dbService.UpdateRegistryUpdate an existing registry
dbService.DeleteRegistryDelete a registry

Common Attributes:

AttributeTypeDescription
registry.namestringName of the registry
server.namestringName of the server
server.versionstringVersion of the server
pagination.limitintPage size limit
pagination.has_cursorboolWhether pagination cursor is used
result.countintNumber of results returned
registry.typestringType of registry (git, api, file, managed)

Sync Operation Spans

Span NameDescription
sync.performRegistrySyncSync a registry from its source

Attributes:

AttributeTypeDescription
registry.namestringName of the registry being synced
registry.typestringType of registry source
sync.successboolWhether sync completed successfully
sync.duration_secondsfloat64Duration of the sync operation
sync.server_countintNumber of servers synced (on success)

Example Traces

Successful API Request

Trace ID: abc123def456...

[12ms] GET /registry/v0.1/servers
├── http.request.method: GET
├── http.route: /registry/v0.1/servers
├── http.response.status_code: 200

└── [10ms] dbService.ListServers
    ├── registry.name: upstream
    ├── pagination.limit: 50
    ├── pagination.has_cursor: false
    └── result.count: 25

Background Sync Operation

Trace ID: xyz789abc...

[30.5s] sync.performRegistrySync
├── registry.name: upstream
├── registry.type: git
├── sync.success: true
├── sync.duration_seconds: 30.5
└── sync.server_count: 42

Failed Request with Error

Trace ID: err456def...

[5ms] GET /registry/v0.1/servers/unknown/versions/1.0.0
├── http.request.method: GET
├── http.route: /registry/v0.1/servers/{serverName}/versions/{version}
├── http.response.status_code: 404

└── [3ms] dbService.GetServerVersion
    ├── server.name: unknown
    ├── server.version: 1.0.0
    ├── status: ERROR
    └── exception.message: server not found: unknown@1.0.0

Tracer Names

Each component uses a unique tracer name for identification:

ComponentTracer Name
HTTP Middlewaregithub.com/stacklok/toolhive-registry-server/http
Database Servicegithub.com/stacklok/toolhive-registry-server/service/db
Sync Coordinatorgithub.com/stacklok/toolhive-registry-server/sync/coordinator

Context Propagation

The Registry Server supports W3C Trace Context propagation. Incoming requests with traceparent headers will have their trace context extracted and used as the parent for all child spans. This enables distributed tracing across multiple services.

Future Tracing Enhancements

The following components are not yet instrumented but are planned for future tracing coverage:

ComponentDescriptionPotential Value
Sync WriterDatabase write operations during syncDiagnose write performance bottlenecks
State ServiceSync state tracking operationsDebug sync state management issues

These additions would provide deeper visibility into sync operations, particularly useful for diagnosing performance issues in large-scale deployments.

Implementation Details

Graceful Degradation

The telemetry implementation handles disabled or missing components gracefully:

  • When telemetry is disabled, no-op providers are used (zero overhead)
  • Metrics and tracing can be independently enabled/disabled
  • Nil provider checks prevent panics if metrics are not configured

Route Pattern Extraction

The HTTP middleware extracts Chi route patterns (e.g., /registry/v0.1/servers/{serverName}) instead of actual URLs (e.g., /registry/v0.1/servers/my-server) to prevent metric cardinality explosion.

Resource Attributes

All telemetry data includes these resource attributes:

AttributeDescription
service.nameService name from config
service.versionService version from config
host.nameHostname of the running instance
telemetry.sdk.name"opentelemetry"
telemetry.sdk.language"go"
telemetry.sdk.versionOTEL SDK version

OTLP Export

Both traces and metrics are exported via OTLP HTTP (port 4318 by default):

  • Traces use batch processing for efficiency
  • Metrics use a periodic reader with 60-second intervals

Troubleshooting

No Metrics Appearing

  1. Verify telemetry is enabled in config:

    telemetry:
      enabled: true
      metrics:
        enabled: true
    
  2. Check the OTEL endpoint is reachable from the registry server

  3. Verify the OTEL Collector is configured to receive OTLP and export to Prometheus

  4. Check Prometheus is scraping the OTEL Collector's metrics endpoint (default port 8889)

High Cardinality Warnings

If you see high cardinality warnings, check for:

  • Custom routes not registered with Chi (will show as unknown_route)
  • Dynamic path segments not using Chi parameters

Missing Traces

  1. Verify tracing is enabled:

    telemetry:
      tracing:
        enabled: true
    
  2. Check sampling rate - default is 5% (sampling: 0.05)

  3. Verify Jaeger or trace backend is configured in OTEL Collector