v3.0.0 f19f453

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"`              // Export Gordon, proxy access, and app container 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	// WorkloadLogs exports proxy access and app container logs, each
39	// under its own service identity. Nil when log export is disabled.
40	WorkloadLogs *LogExporter
41}
42
43// endpointConfig holds parsed endpoint details shared across exporter setup.
44type endpointConfig struct {
45	host     string
46	basePath string
47	insecure bool
48	headers  map[string]string
49}
50
51// NewProvider creates and configures OTel providers based on the given config.
52// Returns a noop provider if telemetry is disabled.
53// The returned shutdown function must be called on application exit.
54func NewProvider(ctx context.Context, cfg Config, serviceName, version string) (*Provider, func(context.Context), error) {
55	noop := func(context.Context) {}
56
57	if !cfg.Enabled || cfg.Endpoint == "" {
58		return &Provider{}, noop, nil
59	}
60
61	res, err := resource.New(ctx,
62		resource.WithAttributes(
63			semconv.ServiceName(serviceName),
64			semconv.ServiceVersion(version),
65		),
66		resource.WithOS(),
67		resource.WithHost(),
68	)
69	if err != nil {
70		return nil, noop, fmt.Errorf("create resource: %w", err)
71	}
72
73	ep, err := parseEndpoint(cfg)
74	if err != nil {
75		return nil, noop, err
76	}
77
78	var shutdowns []func(context.Context) error
79	p := &Provider{}
80
81	if cfg.Traces {
82		if err := p.setupTracing(ctx, ep, cfg, res, &shutdowns); err != nil {
83			return nil, noop, err
84		}
85	}
86
87	if cfg.Metrics {
88		if err := p.setupMetrics(ctx, ep, res, &shutdowns); err != nil {
89			return nil, noop, err
90		}
91	}
92
93	if cfg.Logs {
94		if err := p.setupLogging(ctx, ep, res, &shutdowns); err != nil {
95			return nil, noop, err
96		}
97	}
98
99	shutdown := func(ctx context.Context) {
100		for _, fn := range shutdowns {
101			_ = fn(ctx)
102		}
103	}
104
105	return p, shutdown, nil
106}
107
108// parseEndpoint extracts host, path, and scheme from the configured endpoint URL.
109func parseEndpoint(cfg Config) (*endpointConfig, error) {
110	parsedURL, err := url.Parse(cfg.Endpoint)
111	if err != nil {
112		return nil, fmt.Errorf("parse endpoint URL: %w", err)
113	}
114
115	headers := make(map[string]string)
116	if cfg.AuthToken != "" {
117		headers["Authorization"] = "Basic " + cfg.AuthToken
118	}
119
120	return &endpointConfig{
121		host:     parsedURL.Host,
122		basePath: strings.TrimSuffix(parsedURL.Path, "/"),
123		insecure: parsedURL.Scheme == "http",
124		headers:  headers,
125	}, nil
126}
127
128// setupTracing initializes the trace provider with an OTLP/HTTP exporter.
129func (p *Provider) setupTracing(ctx context.Context, ep *endpointConfig, cfg Config, res *resource.Resource, shutdowns *[]func(context.Context) error) error {
130	traceOpts := []otlptracehttp.Option{
131		otlptracehttp.WithEndpoint(ep.host),
132		otlptracehttp.WithHeaders(ep.headers),
133	}
134	if ep.basePath != "" {
135		traceOpts = append(traceOpts, otlptracehttp.WithURLPath(ep.basePath+"/v1/traces"))
136	}
137	if ep.insecure {
138		traceOpts = append(traceOpts, otlptracehttp.WithInsecure())
139	}
140
141	traceExp, err := otlptracehttp.New(ctx, traceOpts...)
142	if err != nil {
143		return fmt.Errorf("create trace exporter: %w", err)
144	}
145
146	// 0 = never sample (disabled), (0,1) = ratio-based, >= 1 = always sample.
147	// Clamp to [0, 1] so out-of-range values don't produce surprising behavior.
148	rate := cfg.TraceSampleRate
149	if rate < 0 {
150		rate = 0
151	} else if rate > 1 {
152		rate = 1
153	}
154	sampler := trace.AlwaysSample()
155	if rate == 0 {
156		sampler = trace.NeverSample()
157	} else if rate < 1.0 {
158		sampler = trace.TraceIDRatioBased(rate)
159	}
160
161	tp := trace.NewTracerProvider(
162		trace.WithBatcher(traceExp),
163		trace.WithResource(res),
164		trace.WithSampler(sampler),
165	)
166	otel.SetTracerProvider(tp)
167	p.TracerProvider = tp
168	*shutdowns = append(*shutdowns, tp.Shutdown)
169	return nil
170}
171
172// setupMetrics initializes the meter provider with an OTLP/HTTP exporter.
173func (p *Provider) setupMetrics(ctx context.Context, ep *endpointConfig, res *resource.Resource, shutdowns *[]func(context.Context) error) error {
174	metricOpts := []otlpmetrichttp.Option{
175		otlpmetrichttp.WithEndpoint(ep.host),
176		otlpmetrichttp.WithHeaders(ep.headers),
177	}
178	if ep.basePath != "" {
179		metricOpts = append(metricOpts, otlpmetrichttp.WithURLPath(ep.basePath+"/v1/metrics"))
180	}
181	if ep.insecure {
182		metricOpts = append(metricOpts, otlpmetrichttp.WithInsecure())
183	}
184
185	metricExp, err := otlpmetrichttp.New(ctx, metricOpts...)
186	if err != nil {
187		return fmt.Errorf("create metric exporter: %w", err)
188	}
189
190	mp := metric.NewMeterProvider(
191		metric.WithReader(metric.NewPeriodicReader(metricExp)),
192		metric.WithResource(res),
193	)
194	otel.SetMeterProvider(mp)
195	p.MeterProvider = mp
196	*shutdowns = append(*shutdowns, mp.Shutdown)
197	return nil
198}
199
200// setupLogging initializes the log provider with an OTLP/HTTP exporter.
201func (p *Provider) setupLogging(ctx context.Context, ep *endpointConfig, res *resource.Resource, shutdowns *[]func(context.Context) error) error {
202	logOpts := []otlploghttp.Option{
203		otlploghttp.WithEndpoint(ep.host),
204		otlploghttp.WithHeaders(ep.headers),
205	}
206	if ep.basePath != "" {
207		logOpts = append(logOpts, otlploghttp.WithURLPath(ep.basePath+"/v1/logs"))
208	}
209	if ep.insecure {
210		logOpts = append(logOpts, otlploghttp.WithInsecure())
211	}
212
213	logExp, err := otlploghttp.New(ctx, logOpts...)
214	if err != nil {
215		return fmt.Errorf("create log exporter: %w", err)
216	}
217
218	lp := otellog.NewLoggerProvider(
219		otellog.WithProcessor(otellog.NewBatchProcessor(logExp)),
220		otellog.WithResource(res),
221	)
222	p.LogProvider = lp
223	*shutdowns = append(*shutdowns, lp.Shutdown)
224
225	// Workload logs use a dedicated exporter: the SDK forbids concurrent
226	// Export calls, and Gordon's own provider already owns logExp.
227	workloadExp, err := otlploghttp.New(ctx, logOpts...)
228	if err != nil {
229		return fmt.Errorf("create workload log exporter: %w", err)
230	}
231	// Host and OS only: app records must not inherit Gordon's
232	// service.name or service.version.
233	base, err := resource.New(ctx, resource.WithOS(), resource.WithHost())
234	if err != nil {
235		return fmt.Errorf("create workload log resource: %w", err)
236	}
237	p.WorkloadLogs = NewLogExporter(workloadExp, base)
238	*shutdowns = append(*shutdowns, p.WorkloadLogs.Shutdown)
239	return nil
240}

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