DA MASER A MONFUMO | MOTO GUZZI V85TT | PURE SOUND POV 4K

🏍️ Il mio nuovo canale YouTube: giri in moto in POV, solo audio, tra le Dolomiti in 4K. Niente musica, niente parole — solo il motore e le Alpi. Vieni a fare un giro!

Iscriviti

Le applicazioni moderne sono spesso sistemi distribuiti e complessi. Fare il debug non è divertente: devi seguire le richieste tra i servizi, i log si perdono e le metriche sono spesso difficili da correlare. È come cercare un ago in un pagliaio — ma il pagliaio è in fiamme e l'ago continua a spostarsi. È qui che OpenTelemetry (OTel) può aiutare.

OpenTelemetry è un framework open-source per l'osservabilità che aiuta a raccogliere ed esportare trace, metriche e log dalle applicazioni. Standardizza il modo in cui i dati di telemetria vengono raccolti e semplifica l'integrazione con strumenti come Grafana. Con OpenTelemetry possiamo finalmente ottenere informazioni chiare sulle performance della nostra applicazione, permettendoci di rispondere a domande come "Perché questa richiesta è lenta?", "Quante richieste sono attive in questo momento?" e "Quali errori stanno accadendo, e dove?".

In questo post, mostrerò come integrare OpenTelemetry in un'applicazione Go. Alla fine avrai un pacchetto di telemetria riutilizzabile che configura logging, metriche e tracing — il tutto senza ingombrare il codice dell'applicazione! Ho pubblicato il pacchetto, completo di test ed esempi, su GitHub: gotel. Sentiti libero di usarlo come punto di partenza per i tuoi progetti.

Prima Qualche Concetto Importante

Prima di immergerci nel codice, analizziamo i componenti principali di OpenTelemetry: log, metriche e trace. Questi sono i mattoni fondamentali dell'osservabilità e ci aiutano a capire cosa sta succedendo nelle nostre applicazioni.

Provider, risorse, exporter e collector sono i componenti che lavorano insieme per raccogliere, elaborare e inviare i dati di telemetria a un sistema esterno.

Log, Metriche e Trace

Log, metriche e trace sono tutti tipi di dati di telemetria, ma servono scopi diversi. Ecco una rapida panoramica di ciascuno:

  • I Log sono registri di eventi discreti. Pensali come le voci del diario della tua applicazione. Quando qualcosa va storto, i log sono il primo posto dove cercare.
  • Le Metriche tracciano dati numerici nel tempo, come la durata delle richieste, l'utilizzo della CPU o le connessioni attive. Aiutano a monitorare i trend e individuare problemi di performance.
  • Le Trace tracciano una richiesta mentre scorre attraverso più servizi. Una trace è composta da span, ciascuno dei quali rappresenta un'operazione individuale.

In sostanza: i log ci dicono cosa è successo, le metriche mostrano quanto spesso accade, e le trace rivelano come interagiscono le diverse parti del sistema.

Provider, Risorse, Exporter e Collector

In OpenTelemetry, provider, risorse, exporter e collector lavorano insieme per raccogliere, elaborare e inviare dati di telemetria a un sistema esterno. I provider sono responsabili della generazione dei dati di telemetria e si basano sulle risorse, che definiscono metadati sull'applicazione, come il nome del servizio, la versione e l'host. Una volta raccolti i dati di telemetria, devono essere inviati da qualche parte: è questo che fanno gli exporter. Gli exporter inoltrano i dati a un backend di osservabilità come Grafana. Per gestire questo processo in modo più efficiente, OpenTelemetry usa i collector, che fungono da intermediari, aggregando, elaborando e instradando i dati di telemetria prima di inviarli a uno o più backend. I collector aiutano a ridurre l'overhead dell'applicazione e offrono flessibilità nell'archiviazione e nell'analisi della telemetria.

Configurare la Telemetria in Go

Ora costruiamo un pacchetto Go che gestisca log, metriche e trace usando OpenTelemetry. Il pacchetto si chiama gotel ed è disponibile su GitHub: gotel. Questo pacchetto racchiude l'SDK di OpenTelemetry in un'interfaccia semplice, rendendolo più facile da usare.

Configurazione

Per prima cosa, abbiamo bisogno di un modo per configurare il nostro sistema di telemetria. Il file config.go gestisce questo caricando le impostazioni dalle variabili d'ambiente. Questo rende facile modificare la configurazione senza toccare il codice.

go
package gotel
 
import (
	"fmt"
 
	"github.com/caarlos0/env"
)
 
// Config holds the configuration for the telemetry.
type Config struct {
	ServiceName    string `env:"SERVICE_NAME"      envDefault:"gotel"`
	ServiceVersion string `env:"SERVICE_VERSION"   envDefault:"0.0.1"`
	Enabled        bool   `env:"TELEMETRY_ENABLED" envDefault:"true"`
}
 
// NewConfigFromEnv creates a new telemetry config from the environment.
func NewConfigFromEnv() (Config, error) {
	telem := Config{}
	if err := env.Parse(&telem); err != nil {
		return Config{}, fmt.Errorf("failed to parse telemetry config: %w", err)
	}
 
	return telem, nil
}

Questo file definisce una struct Config che memorizza il nome del servizio, la versione e un flag per abilitare o disabilitare la telemetria. La funzione NewConfigFromEnv carica questi valori dalle variabili d'ambiente, permettendo di modificare le impostazioni senza toccare il codice. Se una variabile d'ambiente non è impostata, viene utilizzato un valore predefinito.

Provider ed Exporter

Ora che abbiamo la configurazione, dobbiamo configurare i provider — i componenti responsabili della gestione di log, metriche e trace.

Il file providers.go contiene funzioni per creare i provider di logger, meter e tracer. Queste funzioni vengono usate per inizializzare il sistema di telemetria in NewTelemetry. La funzione newResource è anch'essa definita in questo file per allegare metadati a tutti i dati di telemetria, rendendo più facile tracciare la provenienza dei dati.

go
package gotel
 
import (
	"context"
	"fmt"
	"os"
 
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	"go.opentelemetry.io/otel/sdk/log"
	"go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/resource"
	"go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)
 
// newLoggerProvider creates a new logger provider with the OTLP gRPC exporter.
func newLoggerProvider(ctx context.Context, res *resource.Resource) (*log.LoggerProvider, error) {
	exporter, err := otlploggrpc.New(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to create OTLP log exporter: %w", err)
	}
 
	processor := log.NewBatchProcessor(exporter)
	lp := log.NewLoggerProvider(
		log.WithProcessor(processor),
		log.WithResource(res),
	)
 
	return lp, nil
}
 
// newMeterProvider creates a new meter provider with the OTLP gRPC exporter.
func newMeterProvider(ctx context.Context, res *resource.Resource) (*metric.MeterProvider, error) {
	exporter, err := otlpmetricgrpc.New(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to create OTLP metric exporter: %w", err)
	}
 
	mp := metric.NewMeterProvider(
		metric.WithReader(metric.NewPeriodicReader(exporter)),
		metric.WithResource(res),
	)
	otel.SetMeterProvider(mp)
 
	return mp, nil
}
 
// newTracerProvider creates a new tracer provider with the OTLP gRPC exporter.
func newTracerProvider(ctx context.Context, res *resource.Resource) (*trace.TracerProvider, error) {
	exporter, err := otlptracegrpc.New(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to create OTLP trace exporter: %w", err)
	}
 
	// Create Resource
	tp := trace.NewTracerProvider(
		trace.WithBatcher(exporter),
		trace.WithResource(res),
	)
	otel.SetTracerProvider(tp)
 
	return tp, nil
}
 
// newResource creates a new OTEL resource with the service name and version.
func newResource(serviceName string, serviceVersion string) *resource.Resource {
	hostName, _ := os.Hostname()
 
	return resource.NewWithAttributes(
		semconv.SchemaURL,
		semconv.ServiceName(serviceName),
		semconv.ServiceVersion(serviceVersion),
		semconv.HostName(hostName),
	)
}

Iniziamo importando l'SDK di OpenTelemetry e gli exporter per log, metriche e trace. Questi exporter inviano i dati a un sistema esterno come Grafana o un altro backend compatibile con OTLP.

Il file providers.go contiene funzioni per:

  • newLoggerProvider: crea un logger provider, che raccoglie ed esporta i log. L'exporter OTLP gRPC invia i log sulla rete tramite gRPC, e il BatchProcessor raggruppa efficientemente le voci di log prima di esportarle.
  • newMeterProvider: crea un metrics provider, responsabile della raccolta delle metriche. Esporta le metriche a un backend periodicamente.
  • newTracerProvider: crea un tracing provider per tracciare i flussi delle richieste ed esportarli a un backend esterno.
  • newResource: crea una risorsa con metadati sull'applicazione, come il nome del servizio, la versione e l'hostname. Queste informazioni sono allegate a tutti i dati di telemetria.

Per tutta la telemetria, utilizzo l'exporter OTLP gRPC, che è il default e il più comunemente usato in OpenTelemetry. OTLP (OpenTelemetry Protocol) è un formato standardizzato per trasmettere log, metriche e trace tra applicazioni e backend di osservabilità. Supporta sia gRPC che HTTP come trasporto, consentendo di inviare dati efficientemente in ambienti ad alto throughput. Ho scelto OTLP gRPC perché offre comunicazione a bassa latenza e alte performance con un forte supporto allo streaming, rendendolo ideale per i carichi di lavoro in produzione. Tuttavia, OpenTelemetry supporta molti altri exporter, a seconda del tuo caso d'uso.

Mettere Tutto Insieme

La struct Telemetry racchiude tutti i componenti insieme. È utile quando vogliamo passare il sistema di telemetria ad altre parti dell'applicazione. Ad esempio, possiamo usare la struct Telemetry nel middleware per loggare le richieste e misurare la durata delle richieste. Il file telemetry.go contiene la struct Telemetry e l'interfaccia TelemetryProvider, che definisce i metodi che la struct Telemetry implementa. La struct Telemetry è un wrapper attorno al logger, al meter e al tracer di OpenTelemetry.

go
package gotel
 
import (
	"context"
	"fmt"
	"os"
 
	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/contrib/bridges/otelzap"
	otelmetric "go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/sdk/log"
	"go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/trace"
	oteltrace "go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)
 
// TelemetryProvider is an interface for the telemetry provider.
type TelemetryProvider interface {
	GetServiceName() string
	LogInfo(args ...interface{})
	LogErrorln(args ...interface{})
	LogFatalln(args ...interface{})
	MeterInt64Histogram(metric Metric) (otelmetric.Int64Histogram, error)
	MeterInt64UpDownCounter(metric Metric) (otelmetric.Int64UpDownCounter, error)
	TraceStart(ctx context.Context, name string) (context.Context, oteltrace.Span)
	LogRequest() gin.HandlerFunc
	MeterRequestDuration() gin.HandlerFunc
	MeterRequestsInFlight() gin.HandlerFunc
	Shutdown(ctx context.Context)
}
 
// Telemetry is a wrapper around the OpenTelemetry logger, meter, and tracer.
type Telemetry struct {
	lp     *log.LoggerProvider
	mp     *metric.MeterProvider
	tp     *trace.TracerProvider
	log    *zap.SugaredLogger
	meter  otelmetric.Meter
	tracer oteltrace.Tracer
	cfg    Config
}
 
// NewTelemetry creates a new telemetry instance.
func NewTelemetry(ctx context.Context, cfg Config) (*Telemetry, error) {
	rp := newResource(cfg.ServiceName, cfg.ServiceVersion)
 
	lp, err := newLoggerProvider(ctx, rp)
	if err != nil {
		return nil, fmt.Errorf("failed to create logger: %w", err)
	}
 
	logger := zap.New(
		zapcore.NewTee(
			zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(os.Stdout), zapcore.InfoLevel),
			otelzap.NewCore(cfg.ServiceName, otelzap.WithLoggerProvider(lp)),
		),
	)
 
	mp, err := newMeterProvider(ctx, rp)
	if err != nil {
		return nil, fmt.Errorf("failed to create meter: %w", err)
	}
	meter := mp.Meter(cfg.ServiceName)
 
	tp, err := newTracerProvider(ctx, rp)
	if err != nil {
		return nil, fmt.Errorf("failed to create tracer: %w", err)
	}
	tracer := tp.Tracer(cfg.ServiceName)
 
	return &Telemetry{
		lp:     lp,
		mp:     mp,
		tp:     tp,
		log:    logger.Sugar(),
		meter:  meter,
		tracer: tracer,
		cfg:    cfg,
	}, nil
}
 
// GetServiceName returns the name of the service.
func (t *Telemetry) GetServiceName() string {
	return t.cfg.ServiceName
}
 
// LogInfo logs a message at the info level.
func (t *Telemetry) LogInfo(args ...interface{}) {
	t.log.Info(args...)
}
 
// LogErrorln logs a message and then calls os.Exit(1).
func (t *Telemetry) LogErrorln(args ...interface{}) {
	t.log.Errorln(args...)
}
 
// LogFatalln logs a message and then calls os.Exit(1).
func (t *Telemetry) LogFatalln(args ...interface{}) {
	t.log.Fatalln(args...)
}
 
// MeterInt64Histogram creates a new int64 histogram metric.
func (t *Telemetry) MeterInt64Histogram(metric Metric) (otelmetric.Int64Histogram, error) { //nolint:ireturn
	histogram, err := t.meter.Int64Histogram(
		metric.Name,
		otelmetric.WithDescription(metric.Description),
		otelmetric.WithUnit(metric.Unit),
	)
 
	if err != nil {
		return nil, fmt.Errorf("failed to create histogram: %w", err)
	}
 
	return histogram, nil
}
 
// MeterInt64UpDownCounter creates a new int64 up down counter metric.
func (t *Telemetry) MeterInt64UpDownCounter(metric Metric) (otelmetric.Int64UpDownCounter, error) { //nolint:ireturn
	counter, err := t.meter.Int64UpDownCounter(
		metric.Name,
		otelmetric.WithDescription(metric.Description),
		otelmetric.WithUnit(metric.Unit),
	)
 
	if err != nil {
		return nil, fmt.Errorf("failed to create counter: %w", err)
	}
 
	return counter, nil
}
 
// TraceStart starts a new span with the given name. The span must be ended by calling End.
func (t *Telemetry) TraceStart(ctx context.Context, name string) (context.Context, oteltrace.Span) { //nolint:ireturn
	//nolint: spancheck
	return t.tracer.Start(ctx, name)
}
 
// Shutdown shuts down the logger, meter, and tracer.
func (t *Telemetry) Shutdown(ctx context.Context) {
	t.lp.Shutdown(ctx)
	t.mp.Shutdown(ctx)
	t.tp.Shutdown(ctx)
}

In questo file viene definita un'interfaccia TelemetryProvider per rendere più facile simulare il pacchetto di telemetria nei test, ma anche per rendere più facile sostituire il sistema di telemetria sottostante in futuro.

La funzione NewTelemetry inizializza logging, metriche e tracing e restituisce un'istanza della struct Telemetry. Utilizzo il logger zap per semplicità, ma puoi usare qualsiasi altro logger che si integri con OpenTelemetry. Tieni presente che il logger zap richiede un cosiddetto "bridge" (otelzap) per funzionare con OpenTelemetry.

Un vantaggio nel definire l'interfaccia TelemetryProvider è che possiamo facilmente sostituire il sistema di telemetria sottostante, ad esempio nei test. Ecco un esempio di un provider di telemetria no-op che può essere usato nei test:

go
package gotel
 
import (
	"context"
	"os"
 
	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/trace"
)
 
// NoopTelemetry is a no-op implementation of the TelemetryProvider interface.
type NoopTelemetry struct {
	serviceName string
}
 
// NewNoopTelemetry creates a new NoopTelemetry instance.
func NewNoopTelemetry(cfg Config) (*NoopTelemetry, error) {
	return &NoopTelemetry{serviceName: cfg.ServiceName}, nil
}
 
// GetServiceName returns the service name.
func (t *NoopTelemetry) GetServiceName() string { return t.serviceName }
 
// LogInfo logs nothing.
func (t *NoopTelemetry) LogInfo(args ...interface{}) {}
 
// LogErrorln logs nothing.
func (t *NoopTelemetry) LogErrorln(args ...interface{}) {}
 
// LogFatalln logs nothing, then exits.
func (t *NoopTelemetry) LogFatalln(args ...interface{}) {
	os.Exit(1)
}
 
// LogRequest is a no-op middleware.
func (t *NoopTelemetry) LogRequest() gin.HandlerFunc {
	return func(c *gin.Context) { c.Next() }
}
 
// MeterRequestDuration is a no-op middleware.
func (t *NoopTelemetry) MeterRequestDuration() gin.HandlerFunc {
	return func(c *gin.Context) { c.Next() }
}
 
// MeterRequestsInFlight is a no-op middleware.
func (t *NoopTelemetry) MeterRequestsInFlight() gin.HandlerFunc {
	return func(c *gin.Context) { c.Next() }
}
 
// TraceStart returns the context and span unchanged.
func (t *NoopTelemetry) TraceStart(ctx context.Context, name string) (context.Context, trace.Span) {
	return ctx, trace.SpanFromContext(ctx)
}
 
// MeterInt64Histogram returns nil.
func (t *NoopTelemetry) MeterInt64Histogram(metric Metric) (metric.Int64Histogram, error) {
	return nil, nil
}
 
// MeterInt64UpDownCounter returns nil.
func (t *NoopTelemetry) MeterInt64UpDownCounter(metric Metric) (metric.Int64UpDownCounter, error) {
	return nil, nil
}
 
// Shutdown does nothing.
func (t *NoopTelemetry) Shutdown(ctx context.Context) {}

La struct NoopTelemetry implementa l'interfaccia TelemetryProvider, ma non fa nulla. Questo è utile per i test, dove non vogliamo inviare dati di telemetria a un sistema esterno.

Usare il Pacchetto di Telemetria

In main.go, possiamo ora inizializzare e usare il nostro sistema di telemetria. Ecco un esempio di come usare il pacchetto gotel per creare un nuovo sistema di telemetria. In questo esempio, se l'inizializzazione fallisce, la funzione NewTelemetry ricade su un sistema di telemetria no-op. Questo è utile per la degradazione graziosa, dove vogliamo continuare a eseguire l'applicazione anche se il sistema di telemetria è offline.

go
package main
 
import (
	"context"
	"fmt"
	"os"
 
	"github.com/lucavallin/gotel"
)
 
func main() {
	ctx := context.Background()
 
	telemConfig, err := gotel.NewConfigFromEnv()
	if err != nil {
		fmt.Println("failed to load telemetry config")
		os.Exit(1)
	}
 
	// Initialize telemetry. If the exporter fails, fallback to nop.
	var telem gotel.TelemetryProvider
	telem, err = gotel.NewTelemetry(ctx, telemConfig)
	if err != nil {
		fmt.Println("failed to create telemetry, falling back to no-op telemetry")
		telem, _ = gotel.NewNoopTelemetry(telemConfig)
	}
	defer telem.Shutdown(ctx)
 
	telem.LogInfo("telemetry initialized")
}

La variabile telem è un'istanza della struct Telemetry, che implementa l'interfaccia TelemetryProvider. Può essere usata per scrivere log a livello info con telem.LogInfo(), ad esempio. La variabile telem può essere passata anche ad altre parti dell'applicazione, come servizi, middleware, ecc.

E le Trace?

Le trace tracciano una richiesta mentre scorre attraverso più servizi. Una trace è composta da span, ciascuno dei quali rappresenta un'operazione individuale. La funzione TraceStart fornita dal pacchetto gotel è un modo comodo per avviare un nuovo span e collegarlo al contesto corrente. Questo è utile per instrumentare le richieste HTTP, ad esempio.

Prendiamo questo esempio di un'API fittizia:

go
type API struct {
	telem   gotel.TelemetryProvider
	httpSrv *http.Server
}

Questa API ha un campo telem che contiene un riferimento al sistema di telemetria. Mi piace strutturare le mie API in questo modo perché rende facile usare le dipendenze negli handler HTTP.

go
func (a *API) GetSomething(c *gin.Context) {
	_, span := a.telem.TraceStart(c.Request.Context(), "get_something")
	defer span.End()
 
	something := []string{"foo", "bar", "baz"}
 
	c.JSON(http.StatusOK, something)
}

In questo esempio, l'handler GetSomething avvia un nuovo span con il nome get_something e lo collega al contesto corrente. L'istruzione defer span.End() termina lo span quando la funzione ritorna.

Middleware e Metriche

Un pattern comune è usare i middleware per instrumentare le richieste HTTP. Ecco un esempio di middleware di telemetria che registra la durata delle richieste e conta le richieste in elaborazione. È utile per monitorare la salute dell'applicazione.

In un file metrics.go, definiamo una struct Metrics che contiene riferimenti alle metriche che vogliamo raccogliere. Questo rende facile passare le metriche ai middleware e ad altre parti dell'applicazione. La struct Metric definisce il nome, l'unità e la descrizione di una metrica. Questo è utile per definire le metriche in modo riutilizzabile.

go
package gotel
 
// Metric represents a metric that can be collected by the server.
type Metric struct {
	Name        string
	Unit        string
	Description string
}
 
// MetricRequestDurationMillis is a metric that measures the latency of HTTP requests processed by the server, in milliseconds.
var MetricRequestDurationMillis = Metric{
	Name:        "request_duration_millis",
	Unit:        "ms",
	Description: "Measures the latency of HTTP requests processed by the server, in milliseconds.",
}
 
// MetricRequestsInFlight is a metric that measures the number of requests currently being processed by the server.
var MetricRequestsInFlight = Metric{
	Name:        "requests_inflight",
	Unit:        "{count}",
	Description: "Measures the number of requests currently being processed by the server.",
}

Un file middleware.go contiene invece il middleware di telemetria, da usare con il framework web gin. Le funzioni middleware definite possono essere usate per loggare le richieste, misurare la durata delle richieste e contare le richieste in elaborazione.

go
package gotel
 
import (
	"fmt"
	"time"
 
	"github.com/gin-gonic/gin"
 
	"go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/semconv/v1.20.0/httpconv"
)
 
// LogRequest is a gin middleware that logs the request path.
func (t *Telemetry) LogRequest() gin.HandlerFunc {
	return func(c *gin.Context) {
		t.LogInfo("request to ", c.Request.URL.Path)
		c.Next()
		t.LogInfo("end of request to ", c.Request.URL.Path)
	}
}
 
// MeterRequestDuration is a gin middleware that captures the duration of the request.
func (t *Telemetry) MeterRequestDuration() gin.HandlerFunc {
	// init metric, here we are using histogram for capturing request duration
	histogram, err := t.MeterInt64Histogram(MetricRequestDurationMillis)
	if err != nil {
		t.LogFatalln(fmt.Errorf("failed to create histogram: %w", err))
	}
 
	return func(c *gin.Context) {
		// capture the start time of the request
		startTime := time.Now()
 
		// execute next http handler
		c.Next()
 
		// record the request duration
		duration := time.Since(startTime)
		histogram.Record(
			c.Request.Context(),
			duration.Milliseconds(),
			metric.WithAttributes(
				httpconv.ServerRequest(t.GetServiceName(), c.Request)...,
			),
		)
	}
}
 
// MeterRequestsInFlight is a gin middleware that captures the number of requests in flight.
func (t *Telemetry) MeterRequestsInFlight() gin.HandlerFunc {
	// init metric, here we are using counter for capturing request in flight
	counter, err := t.MeterInt64UpDownCounter(MetricRequestsInFlight)
	if err != nil {
		t.LogFatalln(fmt.Errorf("failed to create counter: %w", err))
	}
 
	return func(c *gin.Context) {
		// define metric attributes
		attrs := metric.WithAttributes(httpconv.ServerRequest(t.GetServiceName(), c.Request)...)
 
		// increase the number of requests in flight
		counter.Add(c.Request.Context(), 1, attrs)
 
		// execute next http handler
		c.Next()
 
		// decrease the number of requests in flight
		counter.Add(c.Request.Context(), -1, attrs)
	}
}

Puoi poi usare il middleware nella tua applicazione per ottenere dati di osservabilità in modo trasparente. Ad esempio, con gin-gonic/gin:

go
r := gin.New()
r.Use(telem.LogRequest())
r.Use(telem.MeterRequestDuration())
r.Use(telem.MeterRequestsInFlight())

Consulta la documentazione di gin su Custom Middleware per ulteriori informazioni.

Dove Finisce la Telemetria?

I dati di telemetria vengono inviati a un backend di osservabilità, e grafana/docker-otel-lgtm di Grafana è un backend OpenTelemetry all-in-one che rende facile iniziare.

Grafana's docker-otel-lgtm

grafana/docker-otel-lgtm di Grafana è un'immagine Docker che offre un backend OpenTelemetry pronto all'uso. Integra l'OpenTelemetry Collector con lo stack LGTM di Grafana (Loki per i log, Grafana per la visualizzazione, Tempo per le trace e Mimir per le metriche).

Eseguendo questo container, puoi ricevere segnali OpenTelemetry sulle porte predefinite (4317 per gRPC e 4318 per HTTP). Questi segnali vengono poi inoltrati automaticamente: i log vanno a Loki, le trace vanno a Tempo e le metriche vanno a Mimir.

Grafana è preconfigurato per visualizzare tutte queste sorgenti di dati ed è accessibile tramite la porta 3000. Questo lo rende un'ottima soluzione per ambienti di sviluppo, demo e test, offrendo un modo rapido per analizzare i dati di telemetria senza necessità di configurazione estesa.

L'osservabilità non è solo un optional: è ciò che ti impedisce di volare alla cieca quando le cose vanno storte nella tua applicazione. OpenTelemetry rende facile raccogliere, elaborare ed esportare log, metriche e trace in modo standardizzato e vendor-neutral. In questo post abbiamo analizzato i concetti chiave dell'osservabilità, esplorato come funziona OpenTelemetry e costruito un pacchetto di telemetria riutilizzabile in Go per mantenere logging, metriche e tracing puliti e coerenti in tutta l'applicazione.

Strutturando tutto in un unico pacchetto facile da usare, abbiamo reso semplice instrumentare il codice senza ingombrarlo. Che tu debba fare il debug di una richiesta lenta, tracciare le performance del sistema, o capire perché il tuo servizio è in fiamme alle 2 di notte, OpenTelemetry ti copre. E con docker-otel-lgtm di Grafana, puoi avviare un backend OpenTelemetry completamente funzionale in pochi secondi per visualizzare tutti i tuoi dati di telemetria.

Se vuoi provarlo, dai un'occhiata a gotel su GitHub. È costruito per essere plug-and-play, così puoi iniziare a raccogliere log, metriche e trace subito. Buona programmazione, e che le tue trace siano sempre collegate, le tue metriche abbiano senso e i tuoi log ti dicano davvero cosa è successo! 🚀