DEV Community

Akarshan Gandotra
Akarshan Gandotra

Posted on

Setting Up Telemetry in Golang

Introduction

Observability is a key aspect of modern applications, enabling developers to monitor and troubleshoot their systems effectively. OpenTelemetry (OTel) provides a standardized framework for tracing and metrics collection. This blog post covers the setup of telemetry in a Golang application using OpenTelemetry, focusing on traces and metrics.

By integrating OpenTelemetry into your application, you can:

  • Collect detailed traces of API calls and internal function executions.
  • Monitor system performance through structured metrics.
  • Gain insight into potential bottlenecks and optimize resource utilization.

This guide will walk you through setting up telemetry for a Golang service, including trace and metric collection, middleware integration, and best practices.

Initializing Telemetry Objects

1. serviceResource

The serviceResource object defines metadata about the service, such as its name. It uses OpenTelemetry's semantic conventions to describe the running application:

var serviceResource = resource.NewWithAttributes(
    semconv.SchemaURL,
    semconv.ServiceNameKey.String("pth-auth-service"),
)
Enter fullscreen mode Exit fullscreen mode

This helps identify telemetry data originating from this service when viewed in monitoring tools like Grafana or Jaeger.

2. initTracer

This function initializes the OpenTelemetry Tracer Provider, which enables distributed tracing. Distributed tracing is useful for tracking requests across different microservices and identifying latency issues.

func initTracer(ctx context.Context, otlpEndpoint string) error {
    exporter, err := otlptracehttp.New(ctx, otlptracehttp.WithEndpoint(otlpEndpoint), otlptracehttp.WithInsecure())
    if err != nil {
        return fmt.Errorf("failed to create OTLP trace exporter: %w", err)
    }

    traceProvider = trace.NewTracerProvider(
        trace.WithBatcher(exporter),
        trace.WithResource(serviceResource),
    )

    otel.SetTracerProvider(traceProvider)
    return nil
}
Enter fullscreen mode Exit fullscreen mode

This function sets up an HTTP-based OpenTelemetry exporter, which sends trace data to an OpenTelemetry collector or a monitoring backend. The WithBatcher(exporter) option ensures that traces are batched before being exported to improve performance.

3. initMetricProvider

This function sets up the Metric Provider, allowing the application to collect and export metrics for monitoring performance:

func initMetricProvider(ctx context.Context, otelMetricsEndpoint string) error {
    exporter, err := otlpmetrichttp.New(ctx, otlpmetrichttp.WithEndpoint(otelMetricsEndpoint), otlpmetrichttp.WithInsecure())
    if err != nil {
        return fmt.Errorf("failed to create OTLP metric exporter: %w", err)
    }

    MeterProvider = sdkmetric.NewMeterProvider(
        sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exporter)),
        sdkmetric.WithResource(serviceResource),
    )
    otel.SetMeterProvider(MeterProvider)

    return nil
}
Enter fullscreen mode Exit fullscreen mode

Metrics are crucial for tracking application behavior over time. Examples include HTTP request durations, memory usage, and active database connections.

4. registerMetrics

This function registers various HTTP server metrics to track API performance:

func registerMetrics(meter metric.Meter) {
    metrics := []struct {
        name        string
        description string
        register    func(metric.Meter) (interface{}, error)
    }{
        {HTTPServerDuration, "Measures the duration of inbound HTTP requests", func(m metric.Meter) (interface{}, error) { return m.Float64Histogram(HTTPServerDuration) }},
        {HTTPServerActiveRequests, "Number of concurrent HTTP requests in flight", func(m metric.Meter) (interface{}, error) { return m.Int64UpDownCounter(HTTPServerActiveRequests) }},
        {HTTPServerRequestSize, "Measures the size of HTTP request messages", func(m metric.Meter) (interface{}, error) { return m.Float64Histogram(HTTPServerRequestSize) }},
        {HTTPServerResponseSize, "Measures the size of HTTP response messages", func(m metric.Meter) (interface{}, error) { return m.Float64Histogram(HTTPServerResponseSize) }},
    }

    for _, metricDef := range metrics {
        if _, err := metricDef.register(meter); err != nil {
            fmt.Printf("[Telemetry] Failed to create metric %s: %v\n", metricDef.name, err)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

These metrics help monitor key aspects of API performance, such as latency and data transfer sizes.

5. InitTelemetry

This function ties everything together by initializing both tracing and metrics. It reads necessary configurations from environment variables and ensures that telemetry is properly set up before the application starts handling requests.

func InitTelemetry(ctx context.Context) error {
    if strings.ToLower(os.Getenv("OTEL_SDK_DISABLED")) == "true" {
        return nil
    }

    otelEndpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
    if otelEndpoint == "" {
        return fmt.Errorf("failed to create OTLP trace exporter: endpoint not set")
    }

    if err := initTracer(ctx, otelEndpoint); err != nil {
        return err
    }

    appLogger.Debug().Msg("[Telemetry] Tracer initialized")

    otelMetricsEndpoint := os.Getenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT")
    if otelMetricsEndpoint == "" {
        otelMetricsEndpoint = os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
    }

    if otelMetricsEndpoint == "" {
        return fmt.Errorf("failed to create OTLP metric exporter: endpoint not set")
    }

    if err := initMetricProvider(ctx, otelMetricsEndpoint); err != nil {
        return err
    }

    appLogger.Debug().Msg("[Telemetry] Metric Provider initialized")

    meter := MeterProvider.Meter("pth-auth-service")
    registerMetrics(meter)

    return nil
}
Enter fullscreen mode Exit fullscreen mode

Integrating Telemetry with the Router

To enable telemetry in the router, we use OpenTelemetry middleware, which ensures all incoming requests are instrumented for tracing and metrics:

func SetupRouter() *gin.Engine {
    router := gin.Default()

    router.Use(otelgin.Middleware("pth-auth-service"))
    if telemetry.MeterProvider != nil {
        router.Use(middlewares.RequestMetricsMiddleware(telemetry.MeterProvider.Meter("pth-auth-service")))
    }

    router.Use(middlewares.LogRequestResponseMiddleware)

    router.GET("/401", controllers.UnauthorizedResponse)
    router.GET("/403", controllers.ForbiddenResponse)

    router.Use(middlewares.TenantValidation())
    router.Use(middlewares.TokenVerification())

    router.POST("/auth", controllers.Auth)
    router.GET("/health", controllers.Health)
    router.GET("/version", controllers.Version)

    return router
}
Enter fullscreen mode Exit fullscreen mode

By adding OpenTelemetry middleware, we ensure that all HTTP requests are traced, allowing developers to debug issues more effectively.

References

This setup ensures observability for our Golang service, helping developers gain insights into system behavior and performance.

Top comments (0)