Some checks failed
Release / Check ui (push) Successful in 39s
Release / Docker / caddy (push) Successful in 1m4s
Release / Test backend (push) Successful in 1m38s
Release / Docker / runner (push) Successful in 39s
Release / Docker / backend (push) Successful in 2m12s
Release / Docker / ui (push) Successful in 1m0s
Release / Upload source maps (push) Failing after 5m19s
Release / Gitea Release (push) Has been skipped
WithEndpoint expects host[:port] with no scheme. When Doppler has https://otel.libnovel.cc the backend was crashing with 'invalid port'. Now strip the scheme and enable TLS when prefix is https://.
121 lines
4.4 KiB
Go
121 lines
4.4 KiB
Go
// Package otelsetup initialises the OpenTelemetry SDK for the LibNovel backend.
|
|
//
|
|
// It reads two environment variables:
|
|
//
|
|
// OTEL_EXPORTER_OTLP_ENDPOINT — OTLP/HTTP endpoint; accepts either a full
|
|
// URL ("https://otel.example.com") or a bare
|
|
// host[:port] ("otel-collector:4318").
|
|
// TLS is used when the value starts with "https://".
|
|
// OTEL_SERVICE_NAME — service name reported in traces (default: "backend")
|
|
//
|
|
// When OTEL_EXPORTER_OTLP_ENDPOINT is empty the function is a no-op: it
|
|
// returns a nil shutdown func and the default slog.Logger, so callers never
|
|
// need to branch on it.
|
|
//
|
|
// Usage in main.go:
|
|
//
|
|
// shutdown, log, err := otelsetup.Init(ctx, version)
|
|
// if err != nil { return err }
|
|
// if shutdown != nil { defer shutdown() }
|
|
package otelsetup
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.opentelemetry.io/contrib/bridges/otelslog"
|
|
"go.opentelemetry.io/otel"
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
|
otellog "go.opentelemetry.io/otel/log/global"
|
|
"go.opentelemetry.io/otel/sdk/log"
|
|
"go.opentelemetry.io/otel/sdk/resource"
|
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
|
)
|
|
|
|
// Init sets up TracerProvider and LoggerProvider that export via OTLP/HTTP.
|
|
//
|
|
// Returns:
|
|
// - shutdown: flushes and stops both providers (nil when OTel is disabled).
|
|
// - logger: an slog.Logger bridged to OTel logs (falls back to default when disabled).
|
|
// - err: non-nil only on SDK initialisation failure.
|
|
func Init(ctx context.Context, version string) (shutdown func(), logger *slog.Logger, err error) {
|
|
rawEndpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
if rawEndpoint == "" {
|
|
return nil, slog.Default(), nil // OTel disabled — not an error
|
|
}
|
|
|
|
// WithEndpoint expects a host[:port] value — no scheme.
|
|
// Support both "https://otel.example.com" and "otel-collector:4318".
|
|
useTLS := strings.HasPrefix(rawEndpoint, "https://")
|
|
endpoint := strings.TrimPrefix(rawEndpoint, "https://")
|
|
endpoint = strings.TrimPrefix(endpoint, "http://")
|
|
|
|
serviceName := os.Getenv("OTEL_SERVICE_NAME")
|
|
if serviceName == "" {
|
|
serviceName = "backend"
|
|
}
|
|
|
|
// ── Shared resource ───────────────────────────────────────────────────────
|
|
res, err := resource.New(ctx,
|
|
resource.WithAttributes(
|
|
semconv.ServiceName(serviceName),
|
|
semconv.ServiceVersion(version),
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, slog.Default(), fmt.Errorf("otelsetup: create resource: %w", err)
|
|
}
|
|
|
|
// ── Trace provider ────────────────────────────────────────────────────────
|
|
traceOpts := []otlptracehttp.Option{otlptracehttp.WithEndpoint(endpoint)}
|
|
if !useTLS {
|
|
traceOpts = append(traceOpts, otlptracehttp.WithInsecure())
|
|
}
|
|
traceExp, err := otlptracehttp.New(ctx, traceOpts...)
|
|
if err != nil {
|
|
return nil, slog.Default(), fmt.Errorf("otelsetup: create OTLP trace exporter: %w", err)
|
|
}
|
|
|
|
tp := sdktrace.NewTracerProvider(
|
|
sdktrace.WithBatcher(traceExp),
|
|
sdktrace.WithResource(res),
|
|
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.2))),
|
|
)
|
|
otel.SetTracerProvider(tp)
|
|
|
|
// ── Log provider ──────────────────────────────────────────────────────────
|
|
logOpts := []otlploghttp.Option{otlploghttp.WithEndpoint(endpoint)}
|
|
if !useTLS {
|
|
logOpts = append(logOpts, otlploghttp.WithInsecure())
|
|
}
|
|
logExp, err := otlploghttp.New(ctx, logOpts...)
|
|
if err != nil {
|
|
return nil, slog.Default(), fmt.Errorf("otelsetup: create OTLP log exporter: %w", err)
|
|
}
|
|
|
|
lp := log.NewLoggerProvider(
|
|
log.WithProcessor(log.NewBatchProcessor(logExp)),
|
|
log.WithResource(res),
|
|
)
|
|
otellog.SetLoggerProvider(lp)
|
|
|
|
// Bridge slog → OTel logs. Structured fields and trace IDs are forwarded
|
|
// automatically; Grafana can correlate log lines with Tempo traces.
|
|
otelLogger := otelslog.NewLogger(serviceName)
|
|
|
|
shutdown = func() {
|
|
shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_ = tp.Shutdown(shutCtx)
|
|
_ = lp.Shutdown(shutCtx)
|
|
}
|
|
|
|
return shutdown, otelLogger, nil
|
|
}
|