v2.31.0-dev10 f49cffc

provider.go RawHistoryFolder

1// Package telemetry provides OpenTelemetry initialization for Gordon.
2// It configures trace, metric, and log providers that export via OTLP/HTTP.
3package telemetry
4
5import (
6	"context"
7	"fmt"
8	"net/url"
9	"strings"
10
11	"go.opentelemetry.io/otel"
12	"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
13	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
14	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
15	otellog "go.opentelemetry.io/otel/sdk/log"
16	"go.opentelemetry.io/otel/sdk/metric"
17	"go.opentelemetry.io/otel/sdk/resource"
18	"go.opentelemetry.io/otel/sdk/trace"
19	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
20)
21
22// Config holds telemetry configuration.
23type Config struct {
24	Enabled         bool    `mapstructure:"enabled"`
25	Endpoint        string  `mapstructure:"endpoint"`          // OTLP HTTP endpoint, e.g. "http://localhost:4318"
26	AuthToken       string  `mapstructure:"auth_token"`        // Basic auth token (base64 encoded user:pass)
27	Traces          bool    `mapstructure:"traces"`            // Enable trace export
28	Metrics         bool    `mapstructure:"metrics"`           // Enable metric export
29	Logs            bool    `mapstructure:"logs"`              // Bridge zerowrap logs to OTLP
30	TraceSampleRate float64 `mapstructure:"trace_sample_rate"` // 0=disabled, (0,1)=ratio-based, >=1=always sample
31}
32
33// Provider holds the initialized OTel providers.
34type Provider struct {
35	TracerProvider *trace.TracerProvider
36	MeterProvider  *metric.MeterProvider
37	LogProvider    *otellog.LoggerProvider
38}
39
40// endpointConfig holds parsed endpoint details shared across exporter setup.
41type endpointConfig struct {
42	host     string
43	basePath string
44	insecure bool
45	headers  map[string]string
46}
47
48// NewProvider creates and configures OTel providers based on the given config.
49// Returns a noop provider if telemetry is disabled.
50// The returned shutdown function must be called on application exit.
51func NewProvider(ctx context.Context, cfg Config, serviceName, version string) (*Provider, func(context.Context), error) {
52	noop := func(context.Context) {}
53
54	if !cfg.Enabled || cfg.Endpoint == "" {
55		return &Provider{}, noop, nil
56	}
57
58	res, err := resource.New(ctx,
59		resource.WithAttributes(
60			semconv.ServiceName(serviceName),
61			semconv.ServiceVersion(version),
62		),
63		resource.WithOS(),
64		resource.WithHost(),
65	)
66	if err != nil {
67		return nil, noop, fmt.Errorf("create resource: %w", err)
68	}
69
70	ep, err := parseEndpoint(cfg)
71	if err != nil {
72		return nil, noop, err
73	}
74
75	var shutdowns []func(context.Context) error
76	p := &Provider{}
77
78	if cfg.Traces {
79		if err := p.setupTracing(ctx, ep, cfg, res, &shutdowns); err != nil {
80			return nil, noop, err
81		}
82	}
83
84	if cfg.Metrics {
85		if err := p.setupMetrics(ctx, ep, res, &shutdowns); err != nil {
86			return nil, noop, err
87		}
88	}
89
90	if cfg.Logs {
91		if err := p.setupLogging(ctx, ep, res, &shutdowns); err != nil {
92			return nil, noop, err
93		}
94	}
95
96	shutdown := func(ctx context.Context) {
97		for _, fn := range shutdowns {
98			_ = fn(ctx)
99		}
100	}
101
102	return p, shutdown, nil
103}
104
105// parseEndpoint extracts host, path, and scheme from the configured endpoint URL.
106func parseEndpoint(cfg Config) (*endpointConfig, error) {
107	parsedURL, err := url.Parse(cfg.Endpoint)
108	if err != nil {
109		return nil, fmt.Errorf("parse endpoint URL: %w", err)
110	}
111
112	headers := make(map[string]string)
113	if cfg.AuthToken != "" {
114		headers["Authorization"] = "Basic " + cfg.AuthToken
115	}
116
117	return &endpointConfig{
118		host:     parsedURL.Host,
119		basePath: strings.TrimSuffix(parsedURL.Path, "/"),
120		insecure: parsedURL.Scheme == "http",
121		headers:  headers,
122	}, nil
123}
124
125// setupTracing initializes the trace provider with an OTLP/HTTP exporter.
126func (p *Provider) setupTracing(ctx context.Context, ep *endpointConfig, cfg Config, res *resource.Resource, shutdowns *[]func(context.Context) error) error {
127	traceOpts := []otlptracehttp.Option{
128		otlptracehttp.WithEndpoint(ep.host),
129		otlptracehttp.WithHeaders(ep.headers),
130	}
131	if ep.basePath != "" {
132		traceOpts = append(traceOpts, otlptracehttp.WithURLPath(ep.basePath+"/v1/traces"))
133	}
134	if ep.insecure {
135		traceOpts = append(traceOpts, otlptracehttp.WithInsecure())
136	}
137
138	traceExp, err := otlptracehttp.New(ctx, traceOpts...)
139	if err != nil {
140		return fmt.Errorf("create trace exporter: %w", err)
141	}
142
143	// 0 = never sample (disabled), (0,1) = ratio-based, >= 1 = always sample.
144	// Clamp to [0, 1] so out-of-range values don't produce surprising behavior.
145	rate := cfg.TraceSampleRate
146	if rate < 0 {
147		rate = 0
148	} else if rate > 1 {
149		rate = 1
150	}
151	sampler := trace.AlwaysSample()
152	if rate == 0 {
153		sampler = trace.NeverSample()
154	} else if rate < 1.0 {
155		sampler = trace.TraceIDRatioBased(rate)
156	}
157
158	tp := trace.NewTracerProvider(
159		trace.WithBatcher(traceExp),
160		trace.WithResource(res),
161		trace.WithSampler(sampler),
162	)
163	otel.SetTracerProvider(tp)
164	p.TracerProvider = tp
165	*shutdowns = append(*shutdowns, tp.Shutdown)
166	return nil
167}
168
169// setupMetrics initializes the meter provider with an OTLP/HTTP exporter.
170func (p *Provider) setupMetrics(ctx context.Context, ep *endpointConfig, res *resource.Resource, shutdowns *[]func(context.Context) error) error {
171	metricOpts := []otlpmetrichttp.Option{
172		otlpmetrichttp.WithEndpoint(ep.host),
173		otlpmetrichttp.WithHeaders(ep.headers),
174	}
175	if ep.basePath != "" {
176		metricOpts = append(metricOpts, otlpmetrichttp.WithURLPath(ep.basePath+"/v1/metrics"))
177	}
178	if ep.insecure {
179		metricOpts = append(metricOpts, otlpmetrichttp.WithInsecure())
180	}
181
182	metricExp, err := otlpmetrichttp.New(ctx, metricOpts...)
183	if err != nil {
184		return fmt.Errorf("create metric exporter: %w", err)
185	}
186
187	mp := metric.NewMeterProvider(
188		metric.WithReader(metric.NewPeriodicReader(metricExp)),
189		metric.WithResource(res),
190	)
191	otel.SetMeterProvider(mp)
192	p.MeterProvider = mp
193	*shutdowns = append(*shutdowns, mp.Shutdown)
194	return nil
195}
196
197// setupLogging initializes the log provider with an OTLP/HTTP exporter.
198func (p *Provider) setupLogging(ctx context.Context, ep *endpointConfig, res *resource.Resource, shutdowns *[]func(context.Context) error) error {
199	logOpts := []otlploghttp.Option{
200		otlploghttp.WithEndpoint(ep.host),
201		otlploghttp.WithHeaders(ep.headers),
202	}
203	if ep.basePath != "" {
204		logOpts = append(logOpts, otlploghttp.WithURLPath(ep.basePath+"/v1/logs"))
205	}
206	if ep.insecure {
207		logOpts = append(logOpts, otlploghttp.WithInsecure())
208	}
209
210	logExp, err := otlploghttp.New(ctx, logOpts...)
211	if err != nil {
212		return fmt.Errorf("create log exporter: %w", err)
213	}
214
215	lp := otellog.NewLoggerProvider(
216		otellog.WithProcessor(otellog.NewBatchProcessor(logExp)),
217		otellog.WithResource(res),
218	)
219	p.LogProvider = lp
220	*shutdowns = append(*shutdowns, lp.Shutdown)
221	return nil
222}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close