feat(metrics): add OTLP metrics support with centralized config
- Create new metrics/ package for OpenTelemetry-native metrics with OTLP export - Refactor OTLP configuration to internal/tracerconfig/ to eliminate code duplication - Add consistent retry configuration across all HTTP OTLP exporters - Add configuration validation and improved error messages - Include test coverage for all new functionality - Make OpenTelemetry metrics dependencies explicit in go.mod Designed for new applications requiring structured metrics export to observability backends via OTLP protocol.
This commit is contained in:
@@ -38,7 +38,6 @@ package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"log/slog"
|
||||
@@ -48,19 +47,14 @@ import (
|
||||
|
||||
"go.ntppool.org/common/internal/tracerconfig"
|
||||
"go.ntppool.org/common/version"
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
"go.opentelemetry.io/contrib/exporters/autoexport"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||
"go.opentelemetry.io/otel/log/global"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
sdklog "go.opentelemetry.io/otel/sdk/log"
|
||||
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
@@ -70,27 +64,24 @@ import (
|
||||
const (
|
||||
// svcNameKey is the environment variable name that Service Name information will be read from.
|
||||
svcNameKey = "OTEL_SERVICE_NAME"
|
||||
|
||||
otelExporterOTLPProtoEnvKey = "OTEL_EXPORTER_OTLP_PROTOCOL"
|
||||
otelExporterOTLPTracesProtoEnvKey = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"
|
||||
otelExporterOTLPLogsProtoEnvKey = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"
|
||||
)
|
||||
|
||||
var errInvalidOTLPProtocol = errors.New("invalid OTLP protocol - should be one of ['grpc', 'http/protobuf']")
|
||||
|
||||
// createOTLPLogExporter creates an OTLP log exporter using the provided configuration.
|
||||
// This function is used as the ExporterFactory for the tracerconfig bridge.
|
||||
// This function is used as the LogExporterFactory for the tracerconfig bridge.
|
||||
func createOTLPLogExporter(ctx context.Context, cfg *tracerconfig.Config) (sdklog.Exporter, error) {
|
||||
// Convert bridge config to local TracerConfig
|
||||
tracerCfg := &TracerConfig{
|
||||
ServiceName: cfg.ServiceName,
|
||||
Environment: cfg.Environment,
|
||||
Endpoint: cfg.Endpoint,
|
||||
EndpointURL: cfg.EndpointURL,
|
||||
CertificateProvider: cfg.CertificateProvider,
|
||||
RootCAs: cfg.RootCAs,
|
||||
}
|
||||
return newOTLPLogExporter(ctx, tracerCfg)
|
||||
return tracerconfig.CreateOTLPLogExporter(ctx, cfg)
|
||||
}
|
||||
|
||||
// createOTLPMetricExporter creates an OTLP metric exporter using the provided configuration.
|
||||
// This function is used as the MetricExporterFactory for the tracerconfig bridge.
|
||||
func createOTLPMetricExporter(ctx context.Context, cfg *tracerconfig.Config) (sdkmetric.Exporter, error) {
|
||||
return tracerconfig.CreateOTLPMetricExporter(ctx, cfg)
|
||||
}
|
||||
|
||||
// createOTLPTraceExporter creates an OTLP trace exporter using the provided configuration.
|
||||
// This function is used as the TraceExporterFactory for the tracerconfig bridge.
|
||||
func createOTLPTraceExporter(ctx context.Context, cfg *tracerconfig.Config) (sdktrace.SpanExporter, error) {
|
||||
return tracerconfig.CreateOTLPTraceExporter(ctx, cfg)
|
||||
}
|
||||
|
||||
// https://github.com/open-telemetry/opentelemetry-go/blob/main/exporters/otlp/otlptrace/otlptracehttp/example_test.go
|
||||
@@ -161,7 +152,7 @@ func SetupSDK(ctx context.Context, cfg *TracerConfig) (shutdown TpShutdownFunc,
|
||||
cfg = &TracerConfig{}
|
||||
}
|
||||
|
||||
// Store configuration for use by logger package via bridge
|
||||
// Store configuration for use by logger and metrics packages via bridge
|
||||
bridgeConfig := &tracerconfig.Config{
|
||||
ServiceName: cfg.ServiceName,
|
||||
Environment: cfg.Environment,
|
||||
@@ -170,7 +161,7 @@ func SetupSDK(ctx context.Context, cfg *TracerConfig) (shutdown TpShutdownFunc,
|
||||
CertificateProvider: cfg.CertificateProvider,
|
||||
RootCAs: cfg.RootCAs,
|
||||
}
|
||||
tracerconfig.Store(ctx, bridgeConfig, createOTLPLogExporter)
|
||||
tracerconfig.Store(ctx, bridgeConfig, createOTLPLogExporter, createOTLPMetricExporter, createOTLPTraceExporter)
|
||||
|
||||
log := slog.Default()
|
||||
|
||||
@@ -249,9 +240,9 @@ func SetupSDK(ctx context.Context, cfg *TracerConfig) (shutdown TpShutdownFunc,
|
||||
|
||||
switch os.Getenv("OTEL_TRACES_EXPORTER") {
|
||||
case "":
|
||||
spanExporter, err = newOLTPExporter(ctx, cfg)
|
||||
spanExporter, err = createOTLPTraceExporter(ctx, bridgeConfig)
|
||||
case "otlp":
|
||||
spanExporter, err = newOLTPExporter(ctx, cfg)
|
||||
spanExporter, err = createOTLPTraceExporter(ctx, bridgeConfig)
|
||||
default:
|
||||
// log.Debug("OTEL_TRACES_EXPORTER", "fallback", os.Getenv("OTEL_TRACES_EXPORTER"))
|
||||
spanExporter, err = autoexport.NewSpanExporter(ctx)
|
||||
@@ -279,141 +270,6 @@ func SetupSDK(ctx context.Context, cfg *TracerConfig) (shutdown TpShutdownFunc,
|
||||
return
|
||||
}
|
||||
|
||||
func newOLTPExporter(ctx context.Context, cfg *TracerConfig) (sdktrace.SpanExporter, error) {
|
||||
log := slog.Default()
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
|
||||
if cfg.CertificateProvider != nil {
|
||||
tlsConfig = &tls.Config{
|
||||
GetClientCertificate: cfg.CertificateProvider,
|
||||
RootCAs: cfg.RootCAs,
|
||||
}
|
||||
}
|
||||
|
||||
proto := os.Getenv(otelExporterOTLPTracesProtoEnvKey)
|
||||
if proto == "" {
|
||||
proto = os.Getenv(otelExporterOTLPProtoEnvKey)
|
||||
}
|
||||
|
||||
// Fallback to default, http/protobuf.
|
||||
if proto == "" {
|
||||
proto = "http/protobuf"
|
||||
}
|
||||
|
||||
var client otlptrace.Client
|
||||
|
||||
switch proto {
|
||||
case "grpc":
|
||||
opts := []otlptracegrpc.Option{
|
||||
otlptracegrpc.WithCompressor("gzip"),
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
opts = append(opts, otlptracegrpc.WithTLSCredentials(credentials.NewTLS(tlsConfig)))
|
||||
}
|
||||
if len(cfg.Endpoint) > 0 {
|
||||
log.Info("adding option", "Endpoint", cfg.Endpoint)
|
||||
opts = append(opts, otlptracegrpc.WithEndpoint(cfg.Endpoint))
|
||||
}
|
||||
if len(cfg.EndpointURL) > 0 {
|
||||
log.Info("adding option", "EndpointURL", cfg.EndpointURL)
|
||||
opts = append(opts, otlptracegrpc.WithEndpointURL(cfg.EndpointURL))
|
||||
}
|
||||
|
||||
client = otlptracegrpc.NewClient(opts...)
|
||||
case "http/protobuf", "http/json":
|
||||
opts := []otlptracehttp.Option{
|
||||
otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
opts = append(opts, otlptracehttp.WithTLSClientConfig(tlsConfig))
|
||||
}
|
||||
if len(cfg.Endpoint) > 0 {
|
||||
opts = append(opts, otlptracehttp.WithEndpoint(cfg.Endpoint))
|
||||
}
|
||||
if len(cfg.EndpointURL) > 0 {
|
||||
opts = append(opts, otlptracehttp.WithEndpointURL(cfg.EndpointURL))
|
||||
}
|
||||
|
||||
opts = append(opts, otlptracehttp.WithRetry(otlptracehttp.RetryConfig{
|
||||
Enabled: true,
|
||||
InitialInterval: 3 * time.Second,
|
||||
MaxInterval: 60 * time.Second,
|
||||
MaxElapsedTime: 5 * time.Minute,
|
||||
}))
|
||||
|
||||
client = otlptracehttp.NewClient(opts...)
|
||||
default:
|
||||
return nil, errInvalidOTLPProtocol
|
||||
}
|
||||
|
||||
exporter, err := otlptrace.New(ctx, client)
|
||||
if err != nil {
|
||||
log.ErrorContext(ctx, "creating OTLP trace exporter", "err", err)
|
||||
}
|
||||
return exporter, err
|
||||
}
|
||||
|
||||
func newOTLPLogExporter(ctx context.Context, cfg *TracerConfig) (sdklog.Exporter, error) {
|
||||
log := slog.Default()
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
|
||||
if cfg.CertificateProvider != nil {
|
||||
tlsConfig = &tls.Config{
|
||||
GetClientCertificate: cfg.CertificateProvider,
|
||||
RootCAs: cfg.RootCAs,
|
||||
}
|
||||
}
|
||||
|
||||
proto := os.Getenv(otelExporterOTLPLogsProtoEnvKey)
|
||||
if proto == "" {
|
||||
proto = os.Getenv(otelExporterOTLPProtoEnvKey)
|
||||
}
|
||||
|
||||
// Fallback to default, http/protobuf.
|
||||
if proto == "" {
|
||||
proto = "http/protobuf"
|
||||
}
|
||||
|
||||
switch proto {
|
||||
case "grpc":
|
||||
opts := []otlploggrpc.Option{
|
||||
otlploggrpc.WithCompressor("gzip"),
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
opts = append(opts, otlploggrpc.WithTLSCredentials(credentials.NewTLS(tlsConfig)))
|
||||
}
|
||||
if len(cfg.Endpoint) > 0 {
|
||||
log.Info("adding log option", "Endpoint", cfg.Endpoint)
|
||||
opts = append(opts, otlploggrpc.WithEndpoint(cfg.Endpoint))
|
||||
}
|
||||
if len(cfg.EndpointURL) > 0 {
|
||||
log.Info("adding log option", "EndpointURL", cfg.EndpointURL)
|
||||
opts = append(opts, otlploggrpc.WithEndpointURL(cfg.EndpointURL))
|
||||
}
|
||||
|
||||
return otlploggrpc.New(ctx, opts...)
|
||||
case "http/protobuf", "http/json":
|
||||
opts := []otlploghttp.Option{
|
||||
otlploghttp.WithCompression(otlploghttp.GzipCompression),
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
opts = append(opts, otlploghttp.WithTLSClientConfig(tlsConfig))
|
||||
}
|
||||
if len(cfg.Endpoint) > 0 {
|
||||
opts = append(opts, otlploghttp.WithEndpoint(cfg.Endpoint))
|
||||
}
|
||||
if len(cfg.EndpointURL) > 0 {
|
||||
opts = append(opts, otlploghttp.WithEndpointURL(cfg.EndpointURL))
|
||||
}
|
||||
|
||||
return otlploghttp.New(ctx, opts...)
|
||||
default:
|
||||
return nil, errInvalidOTLPProtocol
|
||||
}
|
||||
}
|
||||
|
||||
func newTraceProvider(traceExporter sdktrace.SpanExporter, res *resource.Resource) (*sdktrace.TracerProvider, error) {
|
||||
traceProvider := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithResource(res),
|
||||
|
Reference in New Issue
Block a user