Go app framework
This commit is contained in:
54
pkg/otel/ctx.go
Normal file
54
pkg/otel/ctx.go
Normal file
@ -0,0 +1,54 @@
|
||||
package otel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type otelCtxKey uint8
|
||||
|
||||
const (
|
||||
ctxKeyTracer otelCtxKey = iota
|
||||
ctxKeyMeter
|
||||
)
|
||||
|
||||
func MustTracerFromCtx(ctx context.Context) trace.Tracer {
|
||||
ctxData := ctx.Value(ctxKeyTracer)
|
||||
if ctxData == nil {
|
||||
panic(errors.New("no tracer found in context"))
|
||||
}
|
||||
|
||||
tracer, ok := ctxData.(trace.Tracer)
|
||||
if !ok {
|
||||
panic(errors.New("invalid tracer found in context"))
|
||||
}
|
||||
|
||||
return tracer
|
||||
}
|
||||
|
||||
func AddTracerToCtx(ctx context.Context, tracer trace.Tracer) context.Context {
|
||||
ctx = context.WithValue(ctx, ctxKeyTracer, tracer)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func MustMeterFromCtx(ctx context.Context) metric.Meter {
|
||||
ctxData := ctx.Value(ctxKeyMeter)
|
||||
if ctxData == nil {
|
||||
panic(errors.New("no meter found in context"))
|
||||
}
|
||||
|
||||
meter, ok := ctxData.(metric.Meter)
|
||||
if !ok {
|
||||
panic(errors.New("invalid meter found in context"))
|
||||
}
|
||||
|
||||
return meter
|
||||
}
|
||||
|
||||
func AddMeterToCtx(ctx context.Context, meter metric.Meter) context.Context {
|
||||
ctx = context.WithValue(ctx, ctxKeyMeter, meter)
|
||||
return ctx
|
||||
}
|
227
pkg/otel/otel.go
Normal file
227
pkg/otel/otel.go
Normal file
@ -0,0 +1,227 @@
|
||||
package otel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
opentelemetry "go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/exporters/prometheus"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/config"
|
||||
|
||||
noopMetric "go.opentelemetry.io/otel/metric/noop"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
traceSDK "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
|
||||
trace "go.opentelemetry.io/otel/trace"
|
||||
"go.opentelemetry.io/otel/trace/noop"
|
||||
)
|
||||
|
||||
// OTEL Options
|
||||
var (
|
||||
EnableStdoutExporter Option = enableStdoutExporter{}
|
||||
EnablePrometheusExporter Option = enablePrometheusExporter{}
|
||||
// Overide the default metric export interval
|
||||
WithMetricExportInterval = func(interval time.Duration) Option {
|
||||
return exportInterval{interval: interval}
|
||||
}
|
||||
)
|
||||
|
||||
const defMetricInterval = 15 * time.Second
|
||||
|
||||
// Context must carry config.AppConfig
|
||||
func Init(ctx context.Context) (context.Context, func(context.Context) error) {
|
||||
cfg := config.MustFromCtx(ctx)
|
||||
|
||||
// Nothing to do here if not enabled
|
||||
if !cfg.OTEL.Enabled {
|
||||
opentelemetry.SetMeterProvider(noopMetric.NewMeterProvider())
|
||||
opentelemetry.SetTracerProvider(noop.NewTracerProvider())
|
||||
return ctx, func(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
metricInterval = defMetricInterval
|
||||
options = make([]Option, 0)
|
||||
s = &settings{}
|
||||
shutdownFuncs []func(context.Context) error
|
||||
)
|
||||
|
||||
// Prepare settings for OTEL from configuration
|
||||
if cfg.OTEL.StdoutEnabled {
|
||||
options = append(options, EnableStdoutExporter)
|
||||
}
|
||||
if cfg.OTEL.MetricIntervalSecs > 0 {
|
||||
metricInterval = time.Duration(cfg.OTEL.MetricIntervalSecs) * time.Second
|
||||
}
|
||||
if cfg.OTEL.PrometheusEnabled {
|
||||
options = append(options, EnablePrometheusExporter)
|
||||
}
|
||||
options = append(options,
|
||||
WithMetricExportInterval(metricInterval))
|
||||
|
||||
// Apply settings
|
||||
for _, opt := range options {
|
||||
opt.apply(s)
|
||||
}
|
||||
|
||||
// shutdown calls cleanup functions registered via shutdownFuncs.
|
||||
// The errors from the calls are joined.
|
||||
// Each registered cleanup will be invoked once.
|
||||
shutdown := func(ctx context.Context) error {
|
||||
var err error
|
||||
for _, fn := range shutdownFuncs {
|
||||
err = errors.Join(err, fn(ctx))
|
||||
}
|
||||
shutdownFuncs = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// handleErr calls shutdown for cleanup and makes sure that all errors are returned.
|
||||
handleErr := func(inErr error) {
|
||||
if err := errors.Join(inErr, shutdown(ctx)); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "OTEL Error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up meter provider.
|
||||
meterProvider, err := s.newMeterProvider(ctx)
|
||||
if err != nil {
|
||||
handleErr(err)
|
||||
return ctx, shutdown
|
||||
}
|
||||
|
||||
shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
|
||||
opentelemetry.SetMeterProvider(meterProvider)
|
||||
|
||||
meter := opentelemetry.Meter(cfg.Name)
|
||||
ctx = AddMeterToCtx(ctx, meter)
|
||||
|
||||
// Set up tracing
|
||||
opentelemetry.SetTextMapPropagator(newPropagator())
|
||||
var tracerProvider *traceSDK.TracerProvider
|
||||
tracerProvider, err = s.newTracerProvider(ctx)
|
||||
if err != nil {
|
||||
handleErr(err)
|
||||
return ctx, shutdown
|
||||
}
|
||||
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
|
||||
opentelemetry.SetTracerProvider(tracerProvider)
|
||||
|
||||
tracer := opentelemetry.Tracer(cfg.Name)
|
||||
ctx = AddTracerToCtx(ctx, tracer)
|
||||
|
||||
return ctx, shutdown
|
||||
}
|
||||
|
||||
func newPropagator() propagation.TextMapPropagator {
|
||||
return propagation.NewCompositeTextMapPropagator(
|
||||
propagation.TraceContext{},
|
||||
propagation.Baggage{},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *settings) newTracerProvider(ctx context.Context) (traceProvider *traceSDK.TracerProvider, err error) {
|
||||
traceOpts := []traceSDK.TracerProviderOption{
|
||||
traceSDK.WithResource(newResource()),
|
||||
}
|
||||
|
||||
host, set := os.LookupEnv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
||||
if set && host != "" {
|
||||
exporter, err := otlptracegrpc.New(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traceOpts = append(traceOpts, traceSDK.WithBatcher(exporter))
|
||||
}
|
||||
|
||||
if s.EnableStdoutExporter {
|
||||
stdoutExporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traceOpts = append(traceOpts, traceSDK.WithBatcher(stdoutExporter))
|
||||
}
|
||||
|
||||
traceProvider = traceSDK.NewTracerProvider(traceOpts...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func newResource() *resource.Resource {
|
||||
return resource.NewWithAttributes(semconv.SchemaURL)
|
||||
}
|
||||
|
||||
// Configures meter provider
|
||||
// Always provides a prometheus metrics exporter
|
||||
// Conditionally provides an OTLP metrics exporter
|
||||
func (s *settings) newMeterProvider(ctx context.Context) (*metric.MeterProvider, error) {
|
||||
// OTEL Prometheus Exporter
|
||||
exporter, err := prometheus.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metricOptions := make([]metric.Option, 0, 5)
|
||||
if s.EnableStdoutExporter {
|
||||
stdoutMetricExporter, err := stdoutmetric.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metricOptions = append(metricOptions,
|
||||
metric.WithReader(metric.NewPeriodicReader(stdoutMetricExporter)),
|
||||
)
|
||||
}
|
||||
|
||||
host, set := os.LookupEnv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
||||
var otlpExporter *otlpmetricgrpc.Exporter
|
||||
if set && host != "" {
|
||||
if exp, err := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithInsecure()); err != nil {
|
||||
return nil, fmt.Errorf("otlpmetricgrpc.New: %w", err)
|
||||
} else {
|
||||
otlpExporter = exp
|
||||
}
|
||||
}
|
||||
|
||||
var meterProvider *metric.MeterProvider
|
||||
if otlpExporter != nil {
|
||||
metricOptions = append(metricOptions,
|
||||
metric.WithReader(exporter),
|
||||
metric.WithReader(
|
||||
metric.NewPeriodicReader(
|
||||
otlpExporter,
|
||||
metric.WithInterval(s.MetricExportInterval),
|
||||
),
|
||||
),
|
||||
metric.WithResource(newResource()),
|
||||
)
|
||||
} else {
|
||||
metricOptions = append(metricOptions,
|
||||
metric.WithReader(exporter),
|
||||
metric.WithResource(newResource()),
|
||||
)
|
||||
}
|
||||
|
||||
meterProvider = metric.NewMeterProvider(metricOptions...)
|
||||
|
||||
return meterProvider, nil
|
||||
}
|
||||
|
||||
// Creates a new tracer from the global opentelemetry provider
|
||||
func NewTracer(options ...trace.TracerOption) trace.Tracer {
|
||||
return opentelemetry.GetTracerProvider().Tracer(
|
||||
os.Getenv("OTEL_SERVICE_NAME"),
|
||||
options...,
|
||||
)
|
||||
}
|
38
pkg/otel/settings.go
Normal file
38
pkg/otel/settings.go
Normal file
@ -0,0 +1,38 @@
|
||||
package otel
|
||||
|
||||
import "time"
|
||||
|
||||
type settings struct {
|
||||
EnableStdoutExporter bool
|
||||
EnablePrometheusExporter bool
|
||||
MetricExportInterval time.Duration
|
||||
}
|
||||
|
||||
type Option interface {
|
||||
apply(*settings)
|
||||
}
|
||||
|
||||
type enableStdoutExporter struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (setting enableStdoutExporter) apply(o *settings) {
|
||||
o.EnableStdoutExporter = true
|
||||
}
|
||||
|
||||
type enablePrometheusExporter struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (setting enablePrometheusExporter) apply(o *settings) {
|
||||
o.EnablePrometheusExporter = true
|
||||
}
|
||||
|
||||
type exportInterval struct {
|
||||
Option
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func (setting exportInterval) apply(o *settings) {
|
||||
o.MetricExportInterval = setting.interval
|
||||
}
|
30
pkg/otel/util.go
Normal file
30
pkg/otel/util.go
Normal file
@ -0,0 +1,30 @@
|
||||
package otel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/config"
|
||||
)
|
||||
|
||||
func GetTracer(ctx context.Context, components ...string) trace.Tracer {
|
||||
return otel.Tracer(getName(ctx, components...))
|
||||
}
|
||||
|
||||
func GetMeter(ctx context.Context, components ...string) metric.Meter {
|
||||
return otel.Meter(getName(ctx, components...))
|
||||
}
|
||||
|
||||
func getName(ctx context.Context, components ...string) string {
|
||||
cfg := config.MustFromCtx(ctx)
|
||||
|
||||
path := make([]string, 0, len(components)+1)
|
||||
path = append(path, cfg.Name)
|
||||
path = append(path, components...)
|
||||
|
||||
return strings.Join(path, ".")
|
||||
}
|
Reference in New Issue
Block a user