Instrumentação

Instrumentação manual para OpenTelemetry Go

Instrumentation is the act of adding observability code to an app yourself.

If you’re instrumenting an app, you need to use the OpenTelemetry SDK for your language. You’ll then use the SDK to initialize OpenTelemetry and the API to instrument your code. This will emit telemetry from your app, and any library you installed that also comes with instrumentation.

If you’re instrumenting a library, only install the OpenTelemetry API package for your language. Your library will not emit telemetry on its own. It will only emit telemetry when it is part of an app that uses the OpenTelemetry SDK. For more on instrumenting libraries, see Libraries.

For more information about the OpenTelemetry API and SDK, see the specification.

Configuração

Rastros

Inicializando um Tracer

Para criar trechos, você precisará obter ou inicializar um Tracer primeiro.

Certifique-se que os seguintes pacotes Go foram instalados corretamente:

go get go.opentelemetry.io/otel \
  go.opentelemetry.io/otel/trace \
  go.opentelemetry.io/otel/sdk \

Em seguida, inicialize um exporter, recursos, tracer provider e finalmente o rastreador.

package app

import (
	"context"
	"fmt"
	"log"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
	"go.opentelemetry.io/otel/trace"
)

var tracer trace.Tracer

func newExporter(ctx context.Context)  /* (someExporter.Exporter, error) */ {
	// Seu Exporter de preferência: console, jaeger, zipkin, OTLP, etc.
}

func newTraceProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider {
	// Certifique-se de que os recursos padrão do SDK e o nome do serviço estão definidos.
	r, err := resource.Merge(
		resource.Default(),
		resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceName("ServicoExemplo"),
		),
	)

	if err != nil {
		panic(err)
	}

	return sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exp),
		sdktrace.WithResource(r),
	)
}

func main() {
	ctx := context.Background()

	exp, err := newExporter(ctx)
	if err != nil {
		log.Fatalf("falha ao inicializar o exporter: %v", err)
	}

	// Crie um novo TracerProvider com o Processor de Trechos e o Exporter criado.
	tp := newTraceProvider(exp)

	// Lidamos com a finalização corretamente, evitando leaks.
	defer func() { _ = tp.Shutdown(ctx) }()

	otel.SetTracerProvider(tp)

	// Finalmente, definimos o Tracer que será utilizado por este pacote.
	tracer = tp.Tracer("example.io/package/name")
}

Agora você pode acessar tracer para instrumentar manualmente o seu código.

Criando Trechos

Os trechos são criados por rastreadores. Se você não tiver um inicializado, precisará fazer isso.

Para criar um evento com um rastreador, você também precisará de um manipulador para a instância do context.Context. Estes manipuladores vêm geralmente de um objeto de requisição e podem já conter um trecho pai inicializado por uma biblioteca de instrumentação.

func httpHandler(w http.ResponseWriter, r *http.Request) {
	ctx, span := tracer.Start(r.Context(), "hello-span")
	defer span.End()

	// insira algum código para rastrear com o hello-span
}

Em Go, o pacote context é utilizado para armazenar o trecho ativo. Ao iniciar um trecho, você terá acesso não apenas ao trecho criado, mas também ao contexto modificado que o contém.

Uma vez que um trecho é concluído, ele se torna imutável e não pode mais ser modificado.

Obter o trecho atual

Para obter o trecho atual, você precisará extraí-lo de um context.Context ao qual você tenha acesso:

// Este contexto precisa conter o trecho ativo que você pretende extrair.
ctx := context.TODO()
span := trace.SpanFromContext(ctx)

// Insira o código para fazer algo com o trecho atual, chamando opcionalmente o método `span.End()` caso deseje finalizá-lo.

Isso pode ser útil se você quiser adicionar informações ao trecho atual em um determinado momento.

Criar trechos aninhados

Você pode criar um trecho aninhado para rastrear a operação aninhada.

Se o context.Context atual que você possui já contiver um trecho, a criação de um novo trecho resultará em um aninhamento. Por exemplo:

func parentFunction(ctx context.Context) {
	ctx, parentSpan := tracer.Start(ctx, "pai")
	defer parentSpan.End()

	// invoque a função filha e inicie um trecho aninhado dentro dela
	childFunction(ctx)

	// insira mais coisas no código - ao finalizar esta função, o trecho declarado em 'parentSpan' será finalizado.
}

func childFunction(ctx context.Context) {
	// Inicialize um trecho para rastrear a chamada `childFunction()` -
	// este é um trecho aninhado, cujo pai foi declarado no método anterior em `parentSpan`
	ctx, childSpan := tracer.Start(ctx, "filho")
	defer childSpan.End()

	// insira algum código aqui - ao finalizar esta função, o trecho declarado em 'childSpan' será finalizado.
}

Uma vez que o trecho é finalizado, ele se torna imutável e não pode mais ser modificado.

Atributos de Trecho

Os atributos são pares de chave e valor aplicados como metadados aos seus trechos e são úteis para agregar, filtrar e agrupar rastros. Os atributos podem ser adicionados durante a criação de um trecho ou a qualquer momento durante seu ciclo de vida, antes que ele seja concluído.

// definindo atributos na criação...
ctx, span = tracer.Start(ctx, "atributosNaCriacao", trace.WithAttributes(attribute.String("hello", "world")))
// ... e após a criação
span.SetAttributes(attribute.Bool("isTrue", true), attribute.String("atributoDeTexto", "oi!"))

As chaves dos atributos também podem ser pré-computadas:

var myKey = attribute.Key("meuAtributoLegal")
span.SetAttributes(myKey.String("um valor em texto"))

Atributos Semânticos

Os Atributos Semânticos são atributos definidos pela Especificação do OpenTelemetry para fornecer um conjunto comum de chaves de atributos entre várias linguagens, frameworks e ambientes de execução. Esses atributos representam conceitos como métodos HTTP, códigos de estado, user agents e outros. Estes atributos estão disponíveis no pacote go.opentelemetry.io/otel/semconv/v1.26.0.

Para mais detalhes, consulte as Convenções Semânticas de Rastros.

Eventos

Um evento é uma mensagem legível para humanos em um trecho que representa “algo acontecendo” durante a sua duração. Por exemplo, imagine uma função que requer acesso exclusivo a um recurso que está sob um mutex. Um evento poderia ser criado em dois pontos: um quando tentamos obter acesso ao recurso e outro quando adquirimos o mutex.

span.AddEvent("Obtendo o bloqueio")
mutex.Lock()
span.AddEvent("Bloqueio efetuado, realizando ações...")
// fazendo algo
span.AddEvent("Realizando o desbloqueio")
mutex.Unlock()

Uma característica útil dos eventos é que seus registros de data e hora são exibidos como intervalos a partir do início do trecho, permitindo ver facilmente quanto tempo se passou entre cada um.

Os eventos também podem incluir seus próprios atributos -

span.AddEvent("Espera cancelada devido a um sinal externo", trace.WithAttributes(attribute.Int("pid", 4328), attribute.String("signal", "SIGHUP")))

Definir status do trecho

Um estado pode ser definido em um Trecho, sendo tipicamente utilizado para indicar que um Trecho não foi concluído com sucesso - Error. Por padrão, todos os trechos possuem estado Unset, o que significa que o trecho foi concluído sem erros. O estado Ok é reservado para quando você precisa definir explicitamente um trecho como bem-sucedido, em vez de manter o padrão Unset (ou seja, “sem erro”).

O estado pode ser definido a qualquer momento antes que o trecho seja finalizado.

import (
	// ...
	"go.opentelemetry.io/otel/codes"
	// ...
)

// ...

result, err := operacaoQueDeveriaFalhar()
if err != nil {
	span.SetStatus(codes.Error, "operacaoQueDeveriaFalhar falhou")
}

Capturar erros

Caso você tenha uma operação que falhou e deseja capturar o erro produzido, você pode salvar este erro.

import (
	// ...
	"go.opentelemetry.io/otel/codes"
	// ...
)

// ...

result, err := operacaoQueDeveriaFalhar()
if err != nil {
	span.SetStatus(codes.Error, "operacaoQueDeveriaFalhar falhou")
	span.RecordError(err)
}

É altamente recomendável que você também defina o estado de um trecho como Error ao utilizar RecordError, a menos que você não queira considerar o trecho que faz rastreamento de uma operação que falhou como um trecho de erro. O método RecordError não define automaticamente o estado de um trecho ao ser invocado.

Contexto e Propagadores

Os rastros podem se estender além de um único processo. Isso requer a propagação de contexto, um mecanismo onde os identificadores de um rastro são enviados para processos remotos.

Para propagar o contexto de um rastro pela rede, um propagador deve ser registrado com a API do OpenTelemetry.

import (
  "go.opentelemetry.io/otel"
  "go.opentelemetry.io/otel/propagation"
)
...
otel.SetTextMapPropagator(propagation.TraceContext{})

O OpenTelemetry também suporta cabeçalhos no formato B3, para compatibilidade com sistemas de rastreamento (go.opentelemetry.io/contrib/propagators/b3) que não suportam o padrão W3C TraceContext.

Após configurar a propagação de contexto, você provavelmente vai querer utilizar a instrumentação automática para lidar com todo o trabalho que acontece debaixo dos panos para gerenciar a serialização de contexto.

Métricas

Para começar a produzir métricas, você precisará ter um MeterProvider inicializado que permita a criação de um Meter. Os Meters permitem que você crie instrumentos que podem ser utilizados para gerar diferentes tipos de métricas. O OpenTelemetry Go suporta atualmente os seguintes instrumentos:

  • Contador (Counter), um instrumento síncrono que suporta incrementos não-negativos.

  • Contador Assíncrono (Asynchronous Counter), um instrumento assíncrono que suporta incrementos não-negativos.

  • Histograma (Histogram), um instrumento síncrono que suporta valores arbitrários que são estatisticamente significativos, como histogramas, resumos ou percentis.

  • Medidor Síncrono (Synchronous Gauge), um instrumento síncrono que suporta valores não-aditivos, como a medição de temperatura de um ambiente.

  • Medidor Assíncrono (Asynchronous Gauge), um instrumento assíncrono que suporta valores não-aditivos, como a medição de temperatura de um ambiente.

  • Contador UpDown (UpDownCounter), um instrumento síncrono que suporta incrementos e decrementos, como o número de requisições ativas.

  • Contador UpDown Assíncrono (Asynchronous UpDownCounter), um instrumento assíncrono que suporta incrementos e decrementos.

  • Para mais informações sobre instrumentos síncronos, assíncronos, e entender qual dos tipos melhor se encaixa no seu caso de uso, consulte as Diretrizes Suplementares.

Caso um MeterProvider não seja criado, tanto por uma biblioteca de instrumentação ou manualmente, a API de Métricas do OpenTelemetry usará uma implementação sem operação e não irá gerar dados.

A seguir, você poderá encontrar uma documentação mais detalhada para os pacotes:

Inicializar Métricas

Para habilitar métricas em sua aplicação, você precisará de um MeterProvider inicializado, que permitirá que você crie um Meter.

Caso um MeterProvider não seja inicializado, a API de métricas do OpenTelemetry irá utilizar uma implementação sem operação e não irá gerar dados de métricas. Sendo assim, é necessário que o código-fonte da aplicação seja modificado para incluir a inicialização do SDK utilizando os seguintes pacotes:

Certifique-se que os seguintes pacotes Go foram instalados corretamente:

go get go.opentelemetry.io/otel \
  go.opentelemetry.io/otel/exporters/stdout/stdoutmetric \
  go.opentelemetry.io/otel/sdk \
  go.opentelemetry.io/otel/sdk/metric

Em seguida, inicialize um Resource, Metrics Exporter e um Meter Provider:

package main

import (
	"context"
	"log"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
	"go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/resource"
	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

func main() {
	// Crie um 'Resource'.
	res, err := newResource()
	if err != nil {
		panic(err)
	}

	// Inicialize um Meter Provider.
	// Você poderá passar a instância diretamente para o seu código instrumentado, caso
	// o mesmo aceite uma instância do MeterProvider.
	meterProvider, err := newMeterProvider(res)
	if err != nil {
		panic(err)
	}

	// Lidamos com a finalização corretamente, evitando vazamentos.
	defer func() {
		if err := meterProvider.Shutdown(context.Background()); err != nil {
			log.Println(err)
		}
	}()

	// Registre o MeterProvider globalmente, permitindo a utilização via otel.Meter
	// e o acesso utilizando otel.GetMeterProvider.
	// A maioria das bibliotecas de instrumentação utilizam o MeterProvider global como padrão.
	// Caso o MeterProvider global não esteja definido, será utilizada uma implementação sem operação
	// e não irá gerar dados de métricas.
	otel.SetMeterProvider(meterProvider)
}

func newResource() (*resource.Resource, error) {
	return resource.Merge(resource.Default(),
		resource.NewWithAttributes(semconv.SchemaURL,
			semconv.ServiceName("meu-servico"),
			semconv.ServiceVersion("0.1.0"),
		))
}

func newMeterProvider(res *resource.Resource) (*metric.MeterProvider, error) {
	metricExporter, err := stdoutmetric.New()
	if err != nil {
		return nil, err
	}

	meterProvider := metric.NewMeterProvider(
		metric.WithResource(res),
		metric.WithReader(metric.NewPeriodicReader(metricExporter,
			// O valor padrão é 1m. Definimos em 3s para propósitos de demonstração.
			metric.WithInterval(3*time.Second))),
	)
	return meterProvider, nil
}

Agora que o MeterProvider está configurado, podemos obter um Meter.

Obtendo um Meter

Qualquer ponto da sua aplicação que possua código instrumentado poderá invocar o método otel.Meter para obter um Meter. Por exemplo:

import "go.opentelemetry.io/otel"

var meter = otel.Meter("example.io/package/name")

Instrumentos síncronos e assíncronos

Os instrumentos do OpenTelemetry podem ser síncronos ou assíncronos (observáveis).

Os instrumentos síncronos fazem uma medição quando são chamados. A medição é realizada como uma chamada de método durante a execução da aplicação, assim como qualquer outra chamada de método. Periodicamente, a agregação dessas medições é exportada por um Exporter configurado. Como as medições são desacopladas da exportação dos valores medidos, um ciclo de exportação pode conter zero ou várias medições agregadas.

Os instrumentos assíncronos, por outro lado, fornecem uma medição a partir de uma solicitação do SDK. Quando o SDK realiza a exportação, uma função de retorno fornecida ao instrumento de medição no momento de sua criação é invocada. Esta função de retorno fornece ao SDK uma medição, que é imediatamente exportada. Todas as medições em instrumentos assíncronos são realizadas uma vez por cada ciclo de exportação.

Os instrumentos assíncronos podem ser úteis em diversas circunstâncias, como:

  • Quando a atualização de um contador não é computacionalmente barata e você não deseja que o processo em execução aguarde pela medição.
  • Quando medições precisam acontecer em frequências não-relacionadas ao tempo de execução da aplicação (ou seja, não podem ser medidas com precisão ao serem vinculadas ao ciclo de vida de uma operação).
  • Quando não há um registro de data/hora conhecidos para um valor de medição.

Em casos como estes, muitas vezes é melhor realizar a medição de um valor cumulativo diretamente, em vez de agregar uma série de deltas no pós-processamento (como ocorre no exemplo síncrono).

Utilizando Contadores (Counters)

Contadores (Counters) podem ser utilizados para medir valores incrementais e não-negativos.

Por exemplo, aqui está como seria possível reportar o número de chamadas em um servidor HTTP:

import (
	"net/http"

	"go.opentelemetry.io/otel/metric"
)

func init() {
	apiCounter, err := meter.Int64Counter(
		"api.counter",
		metric.WithDescription("Número de chamadas na API."),
		metric.WithUnit("{call}"),
	)
	if err != nil {
		panic(err)
	}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		apiCounter.Add(r.Context(), 1)

		// implementação da chamada da API
	})
}

Utilizando Contadores UpDown (UpDown Counters)

Os Contadores UpDown (UpDown Counters) podem incrementar e decrementar, permitindo que você meça um valor cumulativo que aumenta ou diminui.

Por exemplo, aqui está como é possível reportar o número de itens de uma coleção:

import (
	"context"

	"go.opentelemetry.io/otel/metric"
)

var itemsCounter metric.Int64UpDownCounter

func init() {
	var err error
	itemsCounter, err = meter.Int64UpDownCounter(
		"items.counter",
		metric.WithDescription("Número de itens."),
		metric.WithUnit("{item}"),
	)
	if err != nil {
		panic(err)
	}
}

func addItem() {
	// código que insere um item na coleção

	itemsCounter.Add(context.Background(), 1)
}

func removeItem() {
	// código que remove um item da coleção

	itemsCounter.Add(context.Background(), -1)
}

Utilizando Medidores (Gauges)

Medidores (Gauges) são utilizados para medir quando ocorrem mudanças em valores não-aditivos.

Por exemplo, veja como é possível relatar a velocidade atual de um ventilador de CPU:

import (
	"net/http"

	"go.opentelemetry.io/otel/metric"
)

var fanSpeedSubscription chan int64

func init() {
	speedGauge, err := meter.Int64Gauge(
		"cpu.fan.speed",
		metric.WithDescription("Velocidade atual de um ventilador de CPU"),
		metric.WithUnit("RPM"),
	)
	if err != nil {
		panic(err)
	}

	getCPUFanSpeed := func() int64 {
		// Vamos gerar um valor aleatório para propósito de demonstração.
		// Em uma aplicação do mundo real, substitua esta implementação para obter o real valor do ventilador de CPU.
		return int64(1500 + rand.Intn(1000))
	}

	fanSpeedSubscription = make(chan int64, 1)
	go func() {
		defer close(fanSpeedSubscription)

		for idx := 0; idx < 5; idx++ {
			// Gauges síncronos são utilizados quando o ciclo de medição está
			// relacionado a uma mudança externa.
			time.Sleep(time.Duration(rand.Intn(3)) * time.Second)
			fanSpeed := getCPUFanSpeed()
			fanSpeedSubscription <- fanSpeed
		}
	}()
}

func recordFanSpeed() {
	ctx := context.Background()
	for fanSpeed := range fanSpeedSubscription {
		speedGauge.Record(ctx, fanSpeed)
	}
}

Utilizando Histogramas (Histograms)

Histogramas (Histograms) são utilizados para medir a distribuição de valores ao longo do tempo.

Por exemplo, veja como é possível reportar a distribuição de tempos de resposta para um servidor HTTP:

import (
	"net/http"
	"time"

	"go.opentelemetry.io/otel/metric"
)

func init() {
	histogram, err := meter.Float64Histogram(
		"task.duration",
		metric.WithDescription("Tempo de duração da execução da tarefa."),
		metric.WithUnit("s"),
	)
	if err != nil {
		panic(err)
	}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()

		// insira algum código na chamada da API

		duration := time.Since(start)
		histogram.Record(r.Context(), duration.Seconds())
	})
}

Utilizando Contadores (Assíncronos) Observáveis

Counters observáveis (Observable counters) podem ser utilizados para medir um valor aditivo, não-negativo e monotonamente crescente.

Por exemplo, veja como é possível reportar o tempo de duração desde que a aplicação foi iniciada:

import (
	"context"
	"time"

	"go.opentelemetry.io/otel/metric"
)

func init() {
	start := time.Now()
	if _, err := meter.Float64ObservableCounter(
		"uptime",
		metric.WithDescription("Tempo de duração desde o início da aplicação."),
		metric.WithUnit("s"),
		metric.WithFloat64Callback(func(_ context.Context, o metric.Float64Observer) error {
			o.Observe(float64(time.Since(start).Seconds()))
			return nil
		}),
	); err != nil {
		panic(err)
	}
}

Utilizando Contadores UpDown (Assíncronos) Observáveis

Contadores UpDown (Counters UpDown) observáveis podem incrementar e decrementar, permitindo que você meça um valor cumulativo aditivo, não-negativo e não-monotonamente crescente.

Por exemplo, veja como é possível reportar algumas métricas de banco de dados:

import (
	"context"
	"database/sql"

	"go.opentelemetry.io/otel/metric"
)

// registerDBMetrics registra métricas assíncronas para a base de dados fornecida.
// Certifique-se de desfazer o registro da métrica `metric.Registration` antes de encerrar a conexão com o banco de dados.
func registerDBMetrics(db *sql.DB, meter metric.Meter, poolName string) (metric.Registration, error) {
	max, err := meter.Int64ObservableUpDownCounter(
		"db.client.connections.max",
		metric.WithDescription("O número máximo de conexões abertas permitidas."),
		metric.WithUnit("{connection}"),
	)
	if err != nil {
		return nil, err
	}

	waitTime, err := meter.Int64ObservableUpDownCounter(
		"db.client.connections.wait_time",
		metric.WithDescription("O tempo que levou para obter uma conexão aberta da pool."),
		metric.WithUnit("ms"),
	)
	if err != nil {
		return nil, err
	}

	reg, err := meter.RegisterCallback(
		func(_ context.Context, o metric.Observer) error {
			stats := db.Stats()
			o.ObserveInt64(max, int64(stats.MaxOpenConnections))
			o.ObserveInt64(waitTime, int64(stats.WaitDuration))
			return nil
		},
		max,
		waitTime,
	)
	if err != nil {
		return nil, err
	}
	return reg, nil
}

Utilizando Medidores (Assíncronos) Observáveis

Medidores (Gauges) observáveis devem ser utilizados para medir valores não-aditivos.

Por exemplo, veja como é possível reportar o uso de memória dos objetos heap utilizados na aplicação:

import (
	"context"
	"runtime"

	"go.opentelemetry.io/otel/metric"
)

func init() {
	if _, err := meter.Int64ObservableGauge(
		"memory.heap",
		metric.WithDescription(
			"Uso de memória dos objetos heap alocados.",
		),
		metric.WithUnit("By"),
		metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
			var m runtime.MemStats
			runtime.ReadMemStats(&m)
			o.Observe(int64(m.HeapAlloc))
			return nil
		}),
	); err != nil {
		panic(err)
	}
}

Adicionando atributos

É possível adicionar atributos utilizando as opções WithAttributeSet ou WithAttributes.

import (
	"net/http"

	"go.opentelemetry.io/otel/metric"
	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

func init() {
	apiCounter, err := meter.Int64UpDownCounter(
		"api.finished.counter",
		metric.WithDescription("Número de chamadas da API finalizadas."),
		metric.WithUnit("{call}"),
	)
	if err != nil {
		panic(err)
	}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// insira algum código na chamada da API e defina o status HTTP de resposta

		apiCounter.Add(r.Context(), 1,
			metric.WithAttributes(semconv.HTTPResponseStatusCode(statusCode)))
	})
}

Registrando Views

Uma view oferece aos usuários do SDK a flexibilidade de personalizar a emissão das métricas pelo SDK. Você pode personalizar quais instrumentos de métricas devem ser processados ou ignorados. Você também pode personalizar a agregação e quais atributos você deseja relatar nas métricas.

Cada instrumento possui sua view padrão, que mantém o nome, descrição e atributos originais, e tem uma agregação padrão baseada no tipo do instrumento. Quando uma view registrada corresponde a um instrumento, a view padrão é substituída pela view registrada. Views registradas adicionais que correspondem ao instrumento são aditivas, resultando em múltiplas métricas exportadas para o instrumento.

É possível utilizar o método NewView para criar uma view e registrá-la utilizando a opção WithView.

Por exemplo, veja como é possível criar uma view que renomeia o instrumento latency da versão v0.34.0 da biblioteca de instrumentação http para request.latency:

view := metric.NewView(metric.Instrument{
	Name: "latency",
	Scope: instrumentation.Scope{
		Name:    "http",
		Version: "0.34.0",
	},
}, metric.Stream{Name: "request.latency"})

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

Em outro exemplo, veja como é possível criar uma view que faz com que o instrumento latency da biblioteca de instrumentação http seja reportado como um histograma exponencial:

view := metric.NewView(
	metric.Instrument{
		Name:  "latency",
		Scope: instrumentation.Scope{Name: "http"},
	},
	metric.Stream{
		Aggregation: metric.AggregationBase2ExponentialHistogram{
			MaxSize:  160,
			MaxScale: 20,
		},
	},
)

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

O SDK filtra métricas e atributos antes de exportar as métricas. Você pode utilizar views para reduzir o uso de memória de métricas de alta cardinalidade ou descartar atributos que possam conter dados sensíveis.

No exemplo a seguir, criamos uma view que descarta o instrumento latency da biblioteca de instrumentação http:

view := metric.NewView(
  metric.Instrument{
    Name:  "latency",
    Scope: instrumentation.Scope{Name: "http"},
  },
  metric.Stream{Aggregation: metric.AggregationDrop{}},
)

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

No exemplo a seguir, criamos uma view que remove o atributo http.request.method registrado pelo instrumento latency da biblioteca de instrumentação http:

view := metric.NewView(
  metric.Instrument{
    Name:  "latency",
    Scope: instrumentation.Scope{Name: "http"},
  },
  metric.Stream{AttributeFilter: attribute.NewDenyKeysFilter("http.request.method")},
)

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

O atributo Name suporta correspondência de padrão curinga. O curinga * é reconhecido como correspondendo a zero ou mais caracteres, e o curinga ? é reconhecido como correspondendo exatamente a um caractere. Por exemplo, um padrão de * corresponde a todos os nomes de instrumentos.

No exemplo a seguir, mostramos como criar uma view que define a unidade como milissegundos para qualquer instrumento com um sufixo de nome .ms:

view := metric.NewView(
  metric.Instrument{Name: "*.ms"},
  metric.Stream{Unit: "ms"},
)

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

O método NewView fornece uma maneira conveniente de criar views. Caso o método NewView não forneça as funcionalidades que você precisa, é possível criar uma View personalizada.

No exemplo a seguir, veja como criar uma view que faz o uso de expressões regulares para garantir que todos os nomes de fluxo de dados tenham um sufixo das unidades que utiliza:

re := regexp.MustCompile(`[._](ms|byte)$`)
var view metric.View = func(i metric.Instrument) (metric.Stream, bool) {
	// Em uma função de View personalizada, é necessário copiar de maneira explícita
	// o nome, descrição e unidade.
	s := metric.Stream{Name: i.Name, Description: i.Description, Unit: i.Unit}
	// Qualquer instrumento que não tenha um sufixo de unidade de medida definido, mas tenha
	// uma unidade dimensional definida, terá seu nome atualizado com um sufixo de unidade de medida.
	if re.MatchString(i.Name) {
		return s, false
	}
	switch i.Unit {
	case "ms":
		s.Name += ".ms"
	case "By":
		s.Name += ".byte"
	default:
		return s, false
	}
	return s, true
}

meterProvider := metric.NewMeterProvider(
	metric.WithView(view),
)

Logs

Logs são distintos de métricas e rastros, pois não há uma API de logs do OpenTelemetry voltada para o usuário. Em vez disso, existem ferramentas que integram pacotes de logs populares existentes (como slog, logrus, zap, logr) aos logs do ecossistema OpenTelemetry. Para a justificativa por trás dessa decisão de design, consulte a especificação de Logs.

Os dois workflows discutidos abaixo atendem a diferentes requisitos de aplicação.

Direto para o Collector

Estado: Experimental

No workflow direto para o collector, os logs são emitidos diretamente da aplicação para o Collector utilizando um protocolo de rede (por exemplo, OTLP). Este workflow é simples de configurar, pois não requer componentes adicionais de encaminhamento de logs, permitindo que uma aplicação emita logs estruturados e que estejam nos conformes do modelo de dados de logs. No entanto, a sobrecarga necessária para que as aplicações enfileirem e exportem logs para um local de rede pode não ser adequada para todas as aplicações.

Para utilizar este workflow:

SDK de Logs

O SDK de Logs define como os logs são processados ao utilizar o workflow Direto para o Collector. Nenhum SDK de logs é necessário ao utilizar o workflow de encaminhamento de logs.

Uma configuração típica do SDK de logs instala um Processor de logs em lote com um Exporter OTLP.

Para habilitar logs em sua aplicação, você precisará ter um LoggerProvider inicializado, que permitirá que você utilize um Log Bridge.

Caso um LoggerProvider não seja criado, a API de Logs do OpenTelemetry irá utilizar uma implementação sem operação e dados de logs não serão gerados. Sendo assim, é necessário que o código-fonte da aplicação seja modificado para incluir a inicialização do SDK utilizando os seguintes pacotes:

Certifique-se que os seguintes pacotes Go foram instalados corretamente:

go get go.opentelemetry.io/otel \
  go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp \
  go.opentelemetry.io/otel/sdk \
  go.opentelemetry.io/otel/sdk/log

Em seguida, inicialize o LoggerProvider:

package main

import (
	"context"
	"fmt"

	"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
	"go.opentelemetry.io/otel/log/global"
	"go.opentelemetry.io/otel/sdk/log"
	"go.opentelemetry.io/otel/sdk/resource"
	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

func main() {
	ctx := context.Background()

	// Crie um 'Resource'.
	res, err := newResource()
	if err != nil {
		panic(err)
	}

	// Crie um 'LoggerProvider'.
	// Você pode passar essa instância diretamente ao criar um Log Bridge.
	loggerProvider, err := newLoggerProvider(ctx, res)
	if err != nil {
		panic(err)
	}

	// Lidamos com a finalização corretamente, evitando vazamentos.
	defer func() {
		if err := loggerProvider.Shutdown(ctx); err != nil {
			fmt.Println(err)
		}
	}()

	// Registre o LoggerProvider globalmente, permitindo que seja acessado via global.LoggerProvider.
	// A maioria dos Log Bridges utilizam o LoggerProvider global como padrão.
	// Caso o LoggerProvider global não esteja definido, será utilizada uma implementação sem operação
	// e dados de logs não serão gerados.
	global.SetLoggerProvider(loggerProvider)
}

func newResource() (*resource.Resource, error) {
	return resource.Merge(resource.Default(),
		resource.NewWithAttributes(semconv.SchemaURL,
			semconv.ServiceName("meu-servico"),
			semconv.ServiceVersion("0.1.0"),
		))
}

func newLoggerProvider(ctx context.Context, res *resource.Resource) (*log.LoggerProvider, error) {
	exporter, err := otlploghttp.New(ctx)
	if err != nil {
		return nil, err
	}
	processor := log.NewBatchProcessor(exporter)
	provider := log.NewLoggerProvider(
		log.WithResource(res),
		log.WithProcessor(processor),
	)
	return provider, nil
}

Agora que o LoggerProvider está configurado, é possível utilizá-lo para configurar um Log Bridge.

Log Bridge

Um Log Bridge é um componente que conecta logs de um pacote existente ao SDK de Logs do OpenTelemetry, utilizando a Logs Bridge API.

Uma lista completa contendo os log bridges disponíveis pode ser encontrada no registro do OpenTelemetry.

Cada pacote do Log Bridge deve possuir uma documentação contendo as suas instruções de instalação e configuração, e um exemplo.

Através de arquivos ou stdout

No workflow utilizado em arquivos ou stdout, os logs são gravados em arquivos ou na saída padrão da aplicação. Outro componente (por exemplo, FluentBit) é responsável por ler/acompanhar os logs, convertê-los para um formato mais estruturado e encaminhá-los para um destino, como o Collector. Este workflow pode ser preferível em situações onde os requisitos da aplicação não permitem a sobrecarga adicional do Direto para o Collector. No entanto, é requisito que todos os campos de log necessários sejam codificados nos logs, e que o componente responsável pela leitura realize a conversão para o modelo de dados de logs. A instalação e configuração dos componentes de encaminhamento de logs está fora do escopo deste documento.

Próximas Etapas (#next-steps)

Você também vai querer configurar um Exporter apropriado para exportar seus dados de telemetria para um ou mais backends de telemetria.