Repository navigation
Expand file tree
/
Copy pathtelemetry_test.go
More file actions
361 lines (306 loc) · 13.7 KB
/
Copy pathtelemetry_test.go
File metadata and controls
361 lines (306 loc) · 13.7 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
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
package capability
import (
"testing"
"time"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"github.com/smartcontractkit/chainlink-common/pkg/beholder"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/loop/telemetry"
)
// settingsRoot builds the command tree the way RunErr does, through the same registration, and
// returns what the flags are bound to.
//
// The registration is the production one, so what these tests read is what a binary gets.
func settingsRoot(t *testing.T) (*cobra.Command, *config) {
t.Helper()
root := &cobra.Command{Use: "cron"}
// The production defaults, so what these read is what a binary gets rather than a zero value.
cfg := defaultConfig("TEST_APP")
_, err := cfg.bind(root)
require.NoError(t, err)
return root, cfg
}
// TestEveryFlagIsDocumented fails when a config field has no doc comment, or doccomments_gen.go is
// stale: run go generate.
//
// The exceptions are telemetry.TelemetryConfig's flags: docs are read from a DocComments method on the
// owning struct, and generating one in pkg/loop would make chainlink-common import x/config, which
// already depends on it. Documenting any of these fails this too, so the list stays accurate.
func TestEveryFlagIsDocumented(t *testing.T) {
b, err := defaultConfig("TEST_APP").bind(&cobra.Command{Use: "cron"})
require.NoError(t, err)
require.Equal(t, undocumentedLoopFlags, b.Undocumented())
}
var undocumentedLoopFlags = []string{
"chip-ingress-batch-emitter-enabled",
"chip-ingress-buffer-size",
"chip-ingress-drain-timeout",
"chip-ingress-durable-emitter-enabled",
"chip-ingress-endpoint",
"chip-ingress-insecure-connection",
"chip-ingress-max-batch-size",
"chip-ingress-max-concurrent-sends",
"chip-ingress-max-grpc-request-size",
"chip-ingress-send-interval",
"chip-ingress-send-timeout",
"grpc.advertise-host",
"grpc.server-max-recv-msg-size",
"pyroscope-auth-token",
"pyroscope-environment",
"pyroscope-link-traces-to-profiles",
"pyroscope-pprof-block-profile-rate",
"pyroscope-pprof-mutex-profile-fraction",
"pyroscope-server-address",
"telemetry-attribute",
"telemetry-attribute.<key>",
"telemetry-auth-header",
"telemetry-auth-header.<key>",
"telemetry-auth-headers-ttl",
"telemetry-auth-pub-key-hex",
"telemetry-ca-cert-file",
"telemetry-emitter-batch-processor",
"telemetry-emitter-export-interval",
"telemetry-emitter-export-max-batch-size",
"telemetry-emitter-export-timeout",
"telemetry-emitter-max-queue-size",
"telemetry-enabled",
"telemetry-endpoint",
"telemetry-insecure-connection",
"telemetry-log-batch-processor",
"telemetry-log-compressor",
"telemetry-log-export-interval",
"telemetry-log-export-max-batch-size",
"telemetry-log-export-timeout",
"telemetry-log-level",
"telemetry-log-max-queue-size",
"telemetry-log-streaming-enabled",
"telemetry-metric-cardinality-limit",
"telemetry-metric-compressor",
"telemetry-metric-export-batch-size",
"telemetry-metric-views-deny-attributes",
"telemetry-prometheus-bridge-enabled",
"telemetry-prometheus-bridge-prefixes",
"telemetry-trace-compressor",
"telemetry-trace-sample-ratio",
"tracing-attribute",
"tracing-attribute.<key>",
"tracing-collector-target",
"tracing-enabled",
"tracing-sampling-ratio",
"tracing-tls-cert-path",
}
// decoded runs args through the command tree and returns the settings as they were decoded.
func decoded(t *testing.T, args ...string) config {
t.Helper()
root, cfg := settingsRoot(t)
root.AddCommand(&cobra.Command{Use: "run", RunE: func(*cobra.Command, []string) error { return nil }})
args = append([]string{"run"}, args...)
root.SetArgs(args)
require.NoError(t, root.Execute())
return *cfg
}
// TestTelemetryFlags pins the surface the beholder client's settings have: a flag per field, named
// after the service that owns it. A binary gets these without asking, because every binary has them.
func TestObservabilityFlags(t *testing.T) {
root, _ := settingsRoot(t)
for _, name := range []string{
"telemetry-endpoint",
"telemetry-insecure-connection",
"telemetry-ca-cert-file",
"telemetry-attribute",
"telemetry-auth-header",
"telemetry-auth-pub-key-hex",
"telemetry-auth-headers-ttl",
"telemetry-prometheus-bridge-enabled",
"tracing-enabled",
"tracing-collector-target",
"tracing-sampling-ratio",
"tracing-tls-cert-path",
"chip-ingress-endpoint",
"chip-ingress-insecure-connection",
"pyroscope-server-address",
"pyroscope-auth-token",
"pyroscope-environment",
"http.port",
"app-id",
"prometheus-port",
"capabilities.proxy-url",
"capabilities.capability-don-id",
} {
assert.NotNil(t, root.PersistentFlags().Lookup(name), "missing flag --%s", name)
}
}
// TestCapabilitiesDecodeFromFlags covers the sibling section: what a capability binary needs from
// the node it runs beside, which is not observability and so is configured separately.
func TestCapabilitiesDecodeFromFlags(t *testing.T) {
cfg := decoded(t, "--capabilities.proxy-url", "dns:///registry.internal:9000",
"--capabilities.capability-don-id", "7")
assert.Equal(t, "dns:///registry.internal:9000", cfg.capabilities.ProxyURL)
assert.Equal(t, uint32(7), cfg.capabilities.CapabilityDonID)
}
// TestCapabilitiesAreOptional covers both settings being optional: a binary with no node behind it
// starts, and resolves only the capabilities it hosts.
//
// --http.port is the one thing a run still cannot do without, which is what the args below are.
func TestCapabilitiesAreOptional(t *testing.T) {
root, cfg := settingsRoot(t)
root.AddCommand(&cobra.Command{Use: "run", RunE: func(*cobra.Command, []string) error { return nil }})
root.SetArgs([]string{"run", "--http.port", "1"})
require.NoError(t, root.Execute())
assert.Empty(t, cfg.capabilities.ProxyURL)
assert.Zero(t, cfg.capabilities.CapabilityDonID)
}
// TestObservabilityDefaults pins the settings that are not their zero value, since those are the
// ones an operator gets without asking.
func TestObservabilityDefaults(t *testing.T) {
root, _ := settingsRoot(t)
sampling := root.PersistentFlags().Lookup("tracing-sampling-ratio")
require.NotNil(t, sampling)
assert.Equal(t, "1", sampling.DefValue, "sampling every trace is the default")
port := root.PersistentFlags().Lookup("http.port")
require.NotNil(t, port)
assert.Equal(t, "8080", port.DefValue)
}
// TestBinaryStartsWithNoSettingsAtAll is what defaulting the port bought: nothing has to be
// configured for a run to be a valid one.
//
// It is the whole surface in one assertion - every setting this binary has is now either defaulted
// or genuinely optional - so a flag gaining `validate:"required"` fails here.
func TestBinaryStartsWithNoSettingsAtAll(t *testing.T) {
root, cfg := settingsRoot(t)
root.AddCommand(&cobra.Command{Use: "run", RunE: func(*cobra.Command, []string) error { return nil }})
root.SetArgs([]string{"run"})
root.SilenceUsage, root.SilenceErrors = true, true
require.NoError(t, root.Execute())
assert.Equal(t, uint16(defaultHTTPPort), cfg.observability.http.Port)
}
// TestTelemetryDecodesFromFlags is the point of registering them: what an operator types reaches the
// struct the beholder client is built from.
func TestObservabilityDecodesFromFlags(t *testing.T) {
cfg := decoded(t,
"--tracing-enabled", "true",
"--tracing-sampling-ratio", "0.25",
"--tracing-tls-cert-path", "/certs/trace.pem",
"--chip-ingress-endpoint", "chip:9000",
"--chip-ingress-insecure-connection", "true",
"--pyroscope-server-address", "pyro:4040",
"--pyroscope-environment", "staging",
"--telemetry-endpoint", "otel:4317",
"--telemetry-insecure-connection", "true",
"--telemetry-attribute", "env=staging",
"--telemetry-auth-headers-ttl", "5m",
"--telemetry-prometheus-bridge-enabled", "true",
)
obs := cfg.observability
assert.Equal(t, "otel:4317", obs.TelemetryEndpoint)
assert.True(t, obs.TelemetryInsecureConnection)
assert.Equal(t, beholder.OtelAttributes{"env": "staging"}, obs.TelemetryAttributes)
assert.Equal(t, 5*time.Minute, obs.TelemetryAuthHeadersTTL)
assert.True(t, obs.TelemetryPrometheusBridgeEnabled)
assert.True(t, obs.TracingEnabled)
assert.InDelta(t, 0.25, obs.TracingSamplingRatio, 0)
assert.Equal(t, "/certs/trace.pem", obs.TracingTLSCertPath)
assert.Equal(t, "chip:9000", obs.ChipIngressEndpoint)
assert.True(t, obs.ChipIngressInsecureConnection)
assert.Equal(t, "pyro:4040", obs.PyroscopeServerAddress)
assert.Equal(t, "staging", obs.PyroscopeEnvironment)
}
// TestTelemetryDecodesFromEnv covers why the namespace and field names are what they are: a
// go-plugin host sets CL_TELEMETRY_ENDPOINT, and a binary it starts has to pick that up without
// being passed a flag.
func TestObservabilityDecodesFromEnv(t *testing.T) {
t.Setenv("CL_TELEMETRY_ENDPOINT", "from-env:4317")
assert.Equal(t, "from-env:4317", decoded(t).observability.TelemetryEndpoint)
}
// TestTelemetryDefaultsAreBeholders covers the settings a binary run without a LOOP host gets: they
// are beholder's defaults rather than zero values, except log streaming, which stderr already covers.
func TestTelemetryDefaultsAreBeholders(t *testing.T) {
cfg := defaultObservability().TelemetryConfig
defaults := beholder.DefaultConfig()
assert.Equal(t, defaults.EmitterExportMaxBatchSize, cfg.TelemetryEmitterExportMaxBatchSize)
assert.Empty(t, cfg.TelemetryMetricCompressor, "an empty env var could not override it")
assert.Equal(t, defaults.ChipIngressBufferSize, cfg.ChipIngressBufferSize)
require.NotNil(t, cfg.TelemetryMetricCardinalityLimit)
assert.Equal(t, defaults.MetricCardinalityLimit, *cfg.TelemetryMetricCardinalityLimit)
assert.False(t, cfg.TelemetryLogStreamingEnabled)
}
// TestWithOtelViewsReachesTheClient is the whole of what the option is for: the views a binary
// passes are the ones the beholder client is built with.
//
// They cannot arrive any later - the OTEL specification requires histogram buckets at client
// creation - which is why this is an option on Run rather than something the capability declares.
func TestWithOtelViewsReachesTheClient(t *testing.T) {
view := sdkmetric.NewView(
sdkmetric.Instrument{Name: "cron_capability_*"},
sdkmetric.Stream{Aggregation: sdkmetric.AggregationExplicitBucketHistogram{Boundaries: []float64{1, 2, 3}}},
)
cfg := defaultConfig("TEST_APP")
require.NoError(t, WithOtelViews(view)(cfg))
assert.Len(t, cfg.observability.otelViews, 1, "the view should be what the client is built with")
}
// TestWithOtelViewsDefaultsToNone covers a binary that passes none: the client keeps whatever
// beholder's own defaults are rather than being handed an empty list to mean something.
func TestWithOtelViewsDefaultsToNone(t *testing.T) {
assert.Empty(t, defaultObservability().otelViews)
}
// TestRunUsesTheDecodedTelemetrySettings is the whole path in one: a flag on the command line
// reaches the config the beholder client is built from.
//
// A malformed attribute is what makes that visible without a reachable collector - decoding fails
// before a client is ever created - and an unregistered flag would have failed with cobra's
// "unknown flag" instead.
func TestRunUsesTheDecodedTelemetrySettings(t *testing.T) {
err := execute(t, logger.Test(t), fakeConstructor, "run",
"--telemetry-endpoint", "otel:4317", "--telemetry-attribute", "nope")
require.ErrorContains(t, err, "telemetry-attribute")
require.NotContains(t, err.Error(), "unknown flag")
}
// TestStartTelemetryWithoutAnEndpointChangesNothing is the off case: no endpoint means no client,
// so nothing is installed, nothing joins the root, and there is nothing to undo.
func TestStartTelemetryWithoutAnEndpointChangesNothing(t *testing.T) {
before := beholder.GetClient()
telemetry, err := startTelemetry(t.Context(), logger.Test(t), &observability{})
require.NoError(t, err)
// A service that does nothing rather than a nil, so the caller has one thing to hand the root
// either way. No client behind it: the process keeps the noop beholder client it already had.
require.NotNil(t, telemetry)
assert.Nil(t, telemetry.Client(), "there is no client when telemetry is off")
assert.Same(t, before, beholder.GetClient(), "nothing should have been installed")
require.NoError(t, telemetry.Close())
assert.Same(t, before, beholder.GetClient(), "and closing it changes nothing")
}
// TestRunLeavesTelemetryAsItFoundIt is the same guarantee seen from the binary: a run that started
// telemetry and finished has put the process back the way it was.
func TestRunLeavesTelemetryAsItFoundIt(t *testing.T) {
before := beholder.GetClient()
require.NoError(t, execute(t, logger.Test(t), fakeConstructor, "run"))
assert.Same(t, before, beholder.GetClient())
}
// TestStreamLogsKeepsTheLoggerWhenStreamingIsOff covers the default. The run must keep its logger.
func TestStreamLogsKeepsTheLoggerWhenStreamingIsOff(t *testing.T) {
lggr := logger.Test(t)
tel, err := telemetry.NewTelemetry(lggr, defaultTelemetryConfig(), "", nil)
require.NoError(t, err)
got, err := streamLogs(lggr, tel)
require.NoError(t, err)
assert.True(t, got == lggr, "the logger should not change")
}
// TestStreamLogsReplacesTheLoggerWhenStreamingIsOn makes sure that the run uses a new logger that
// sends its logs to beholder. The new logger must keep the name of the old logger.
func TestStreamLogsReplacesTheLoggerWhenStreamingIsOn(t *testing.T) {
cfg := defaultTelemetryConfig()
cfg.TelemetryEnabled = true
cfg.TelemetryEndpoint = "localhost:1"
cfg.TelemetryInsecureConnection = true
cfg.TelemetryLogStreamingEnabled = true
lggr := logger.Named(logger.Test(t), "cron")
tel, err := telemetry.NewTelemetry(lggr, cfg, "", nil)
require.NoError(t, err)
got, err := streamLogs(lggr, tel)
require.NoError(t, err)
assert.False(t, got == lggr, "the logger should be replaced")
assert.Equal(t, "cron", got.Name())
}