-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathobservability_config.go
More file actions
320 lines (289 loc) · 9.93 KB
/
observability_config.go
File metadata and controls
320 lines (289 loc) · 9.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
// Copyright 2025 The Rivaas Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package app
import (
"fmt"
"rivaas.dev/logging"
"rivaas.dev/metrics"
"rivaas.dev/tracing"
)
// TracingProvider defines available tracing backends.
type TracingProvider string
const (
// TracingStdout exports traces to stdout (development/testing).
TracingStdout TracingProvider = "stdout"
// TracingOTLP exports traces via OTLP gRPC protocol.
TracingOTLP TracingProvider = "otlp"
// TracingOTLPHTTP exports traces via OTLP HTTP protocol.
TracingOTLPHTTP TracingProvider = "otlp-http"
// TracingNoop is a no-op provider (no traces exported).
TracingNoop TracingProvider = "noop"
)
// MetricsProvider defines available metrics backends.
type MetricsProvider string
const (
// MetricsPrometheus uses Prometheus exporter for metrics.
MetricsPrometheus MetricsProvider = "prometheus"
// MetricsOTLP uses OTLP HTTP exporter for metrics.
MetricsOTLP MetricsProvider = "otlp"
// MetricsStdout uses stdout exporter for metrics (development/testing).
MetricsStdout MetricsProvider = "stdout"
)
// LoggingHandler defines log output formats.
type LoggingHandler string
const (
// LoggingConsole uses console handler (human-readable).
LoggingConsole LoggingHandler = "console"
// LoggingJSON uses JSON handler (machine-readable).
LoggingJSON LoggingHandler = "json"
)
// LoggingLevel defines log levels.
type LoggingLevel string
const (
// LoggingDebug enables debug-level logging.
LoggingDebug LoggingLevel = "debug"
// LoggingInfo enables info-level logging.
LoggingInfo LoggingLevel = "info"
// LoggingWarn enables warn-level logging.
LoggingWarn LoggingLevel = "warn"
// LoggingError enables error-level logging.
LoggingError LoggingLevel = "error"
)
// TracingConfig configures distributed tracing.
// This struct can be loaded from configuration files (YAML, JSON, etc.).
//
// Example YAML:
//
// tracing:
// provider: otlp
// endpoint: localhost:4317
// sampleRate: 0.1
// insecure: true
type TracingConfig struct {
Provider TracingProvider `config:"provider" json:"provider" yaml:"provider"`
Endpoint string `config:"endpoint" json:"endpoint" yaml:"endpoint"`
SampleRate float64 `config:"sampleRate" json:"sampleRate" yaml:"sampleRate"`
Insecure bool `config:"insecure" json:"insecure" yaml:"insecure"`
}
// options converts TracingConfig to tracing.Option slice.
// This is the bridge between declarative config and the functional options API.
func (c TracingConfig) options() ([]tracing.Option, error) {
var opts []tracing.Option
switch c.Provider {
case TracingStdout:
opts = append(opts, tracing.WithStdout())
case TracingOTLP:
endpoint := c.Endpoint
if endpoint == "" {
endpoint = "localhost:4317"
}
if c.Insecure {
opts = append(opts, tracing.WithOTLP(endpoint, tracing.OTLPInsecure()))
} else {
opts = append(opts, tracing.WithOTLP(endpoint))
}
case TracingOTLPHTTP:
endpoint := c.Endpoint
if endpoint == "" {
endpoint = "http://localhost:4318"
}
opts = append(opts, tracing.WithOTLPHTTP(endpoint))
case TracingNoop, "":
opts = append(opts, tracing.WithNoop())
default:
return nil, fmt.Errorf("unknown provider %q (valid: stdout, otlp, otlp-http, noop)", c.Provider)
}
if c.SampleRate > 0 && c.SampleRate <= 1.0 {
opts = append(opts, tracing.WithSampleRate(c.SampleRate))
}
return opts, nil
}
// MetricsConfig configures metrics collection.
// This struct can be loaded from configuration files (YAML, JSON, etc.).
//
// Example YAML:
//
// metrics:
// provider: prometheus
// endpoint: ":9090"
// path: /metrics
type MetricsConfig struct {
Provider MetricsProvider `config:"provider" json:"provider" yaml:"provider"`
Endpoint string `config:"endpoint" json:"endpoint" yaml:"endpoint"`
Path string `config:"path" json:"path" yaml:"path"`
}
// options converts MetricsConfig to metrics.Option slice.
// This is the bridge between declarative config and the functional options API.
func (c MetricsConfig) options() ([]metrics.Option, error) {
switch c.Provider {
case MetricsPrometheus, "":
endpoint := c.Endpoint
if endpoint == "" {
endpoint = ":9090"
}
path := c.Path
if path == "" {
path = "/metrics"
}
return []metrics.Option{metrics.WithPrometheus(endpoint, path)}, nil
case MetricsOTLP:
endpoint := c.Endpoint
if endpoint == "" {
endpoint = "localhost:4318"
}
return []metrics.Option{metrics.WithOTLP(endpoint)}, nil
case MetricsStdout:
return []metrics.Option{metrics.WithStdout()}, nil
default:
return nil, fmt.Errorf("unknown provider %q (valid: prometheus, otlp, stdout)", c.Provider)
}
}
// LoggingConfig configures structured logging.
// This struct can be loaded from configuration files (YAML, JSON, etc.).
//
// Example YAML:
//
// logging:
// handler: json
// level: info
type LoggingConfig struct {
Handler LoggingHandler `config:"handler" json:"handler" yaml:"handler"`
Level LoggingLevel `config:"level" json:"level" yaml:"level"`
}
// options converts LoggingConfig to logging.Option slice.
// This is the bridge between declarative config and the functional options API.
func (c LoggingConfig) options() ([]logging.Option, error) {
var opts []logging.Option
switch c.Handler {
case LoggingJSON:
opts = append(opts, logging.WithJSONHandler())
case LoggingConsole, "":
opts = append(opts, logging.WithConsoleHandler())
default:
return nil, fmt.Errorf("unknown handler %q (valid: console, json)", c.Handler)
}
switch c.Level {
case LoggingDebug:
opts = append(opts, logging.WithDebugLevel())
case LoggingInfo, "":
opts = append(opts, logging.WithLevel(logging.LevelInfo))
case LoggingWarn:
opts = append(opts, logging.WithLevel(logging.LevelWarn))
case LoggingError:
opts = append(opts, logging.WithLevel(logging.LevelError))
default:
return nil, fmt.Errorf("unknown level %q (valid: debug, info, warn, error)", c.Level)
}
return opts, nil
}
// ObservabilityConfig is the unified observability configuration.
// Embed this in your app config struct for seamless config loading.
//
// Design note: this exported struct is an intentional exception to the "no user-facing config
// structs" rule. It exists purely as a DTO (Data Transfer Object) to bridge declarative
// file-based configuration (YAML/JSON) and the functional options API. Users only interact with
// it by passing it to [WithObservabilityFromConfig]; they never mutate it directly. See the
// design decisions document for the full rationale.
//
// Example YAML:
//
// observability:
// tracing:
// provider: otlp
// endpoint: localhost:4317
// metrics:
// provider: prometheus
// logging:
// handler: json
// level: info
// excludePaths:
// - /livez
// - /readyz
//
// Example usage:
//
// type AppConfig struct {
// Server ServerConfig `config:"server"`
// Observability app.ObservabilityConfig `config:"observability"`
// }
//
// app.New(
// app.WithServiceName("my-api"),
// app.WithObservabilityConfig(cfg.Observability),
// )
type ObservabilityConfig struct {
Tracing TracingConfig `config:"tracing" json:"tracing" yaml:"tracing"`
Metrics MetricsConfig `config:"metrics" json:"metrics" yaml:"metrics"`
Logging LoggingConfig `config:"logging" json:"logging" yaml:"logging"`
ExcludePaths []string `config:"excludePaths" json:"excludePaths" yaml:"excludePaths"`
ExcludePrefixes []string `config:"excludePrefixes" json:"excludePrefixes" yaml:"excludePrefixes"`
}
// WithObservabilityFromConfig configures all observability from a single config struct.
// This is a convenience method that converts declarative configuration
// into functional options and applies them via the existing WithObservability function.
//
// This function is ideal for loading observability configuration from files (YAML, JSON, etc.).
// Invalid tracing, metrics, or logging configuration is reported when the app is constructed
// (e.g. from [New]) as a validation error, not via panic.
//
// Example:
//
// app.New(
// app.WithServiceName("blog-api"),
// app.WithObservabilityFromConfig(cfg.Observability),
// )
func WithObservabilityFromConfig(cfg ObservabilityConfig) Option {
return func(c *config) {
if c.observability == nil {
c.observability = defaultObservabilitySettings()
}
var obsOpts []ObservabilityOption
// Tracing
if cfg.Tracing.Provider != "" {
tracingOpts, err := cfg.Tracing.options()
if err != nil {
c.observability.validationErrors = append(c.observability.validationErrors, fmt.Errorf("tracing: %w", err))
} else {
obsOpts = append(obsOpts, WithTracing(tracingOpts...))
}
}
// Metrics
if cfg.Metrics.Provider != "" {
metricsOpts, err := cfg.Metrics.options()
if err != nil {
c.observability.validationErrors = append(c.observability.validationErrors, fmt.Errorf("metrics: %w", err))
} else {
obsOpts = append(obsOpts, WithMetrics(metricsOpts...))
}
}
// Logging
if cfg.Logging.Handler != "" {
loggingOpts, err := cfg.Logging.options()
if err != nil {
c.observability.validationErrors = append(c.observability.validationErrors, fmt.Errorf("logging: %w", err))
} else {
obsOpts = append(obsOpts, WithLogging(loggingOpts...))
}
}
// Path exclusions
if len(cfg.ExcludePaths) > 0 {
obsOpts = append(obsOpts, WithExcludePaths(cfg.ExcludePaths...))
}
if len(cfg.ExcludePrefixes) > 0 {
obsOpts = append(obsOpts, WithExcludePrefixes(cfg.ExcludePrefixes...))
}
// Apply all observability options
WithObservability(obsOpts...)(c)
}
}