Skip to content

Commit c5f1d57

Browse files
authored
Merge branch 'main' into infoplat-14703-chip_client-label
2 parents 1b25484 + 93eeb23 commit c5f1d57

94 files changed

Lines changed: 1216 additions & 507 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

observability-lib/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module github.com/smartcontractkit/chainlink-common/observability-lib
33
go 1.26.2
44

55
require (
6-
github.com/go-resty/resty/v2 v2.17.1
7-
github.com/grafana/grafana-foundation-sdk/go v0.0.12
6+
github.com/go-resty/resty/v2 v2.17.2
7+
github.com/grafana/grafana-foundation-sdk/go v0.0.18
88
github.com/spf13/cobra v1.10.2
99
github.com/stretchr/testify v1.11.1
1010
gopkg.in/yaml.v3 v3.0.1

observability-lib/go.sum

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

observability-lib/grafana/alerts.go

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ type MathExpression struct {
7676

7777
type ResampleExpression struct {
7878
Expression string
79+
Window string
7980
DownSampler expr.TypeResampleDownsampler
8081
UpSampler expr.TypeResampleUpsampler
8182
}
@@ -137,47 +138,57 @@ func newConditionQuery(options ConditionQuery) *alerting.QueryBuilder {
137138
DatasourceUid("__expr__")
138139

139140
if options.ReduceExpression != nil {
140-
reduceBuider := expr.NewTypeReduceBuilder().
141+
reduceBuilder := expr.NewTypeReduceBuilder().
141142
RefId(options.RefID).
142143
Expression(options.ReduceExpression.Expression).
143144
IntervalMs(*options.IntervalMs).
144145
MaxDataPoints(*options.MaxDataPoints).
145146
Reducer(options.ReduceExpression.Reducer)
146147

147148
if options.ReduceExpression.ReduceSettings != nil && options.ReduceExpression.ReduceSettings.Mode != "" {
148-
reduceBuider.Settings(newReduceSettingsOptions(*options.ReduceExpression.ReduceSettings))
149+
reduceBuilder.Settings(newReduceSettingsOptions(*options.ReduceExpression.ReduceSettings))
149150
}
150-
res.Model(reduceBuider)
151+
res.Model(expr.NewTypeMathOrTypeReduceOrTypeResampleOrTypeClassicConditionsOrTypeThresholdOrTypeSqlBuilder().
152+
TypeReduce(reduceBuilder))
151153
}
152154

153155
if options.MathExpression != nil {
154-
res.Model(expr.NewTypeMathBuilder().
155-
RefId(options.RefID).
156-
Expression(options.MathExpression.Expression).
157-
IntervalMs(*options.IntervalMs).
158-
MaxDataPoints(*options.MaxDataPoints),
159-
)
156+
res.Model(expr.NewTypeMathOrTypeReduceOrTypeResampleOrTypeClassicConditionsOrTypeThresholdOrTypeSqlBuilder().
157+
TypeMath(expr.NewTypeMathBuilder().
158+
RefId(options.RefID).
159+
Expression(options.MathExpression.Expression).
160+
IntervalMs(*options.IntervalMs).
161+
MaxDataPoints(*options.MaxDataPoints),
162+
))
160163
}
161164

162165
if options.ResampleExpression != nil {
163-
res.Model(expr.NewTypeResampleBuilder().
164-
RefId(options.RefID).
165-
Expression(options.ResampleExpression.Expression).
166-
IntervalMs(*options.IntervalMs).
167-
MaxDataPoints(*options.MaxDataPoints).
168-
Downsampler(options.ResampleExpression.DownSampler).
169-
Upsampler(options.ResampleExpression.UpSampler),
170-
)
166+
window := options.ResampleExpression.Window
167+
if window == "" {
168+
// Window is required (>= 1 char) by the SDK; default to the relative time range.
169+
window = "10m"
170+
}
171+
res.Model(expr.NewTypeMathOrTypeReduceOrTypeResampleOrTypeClassicConditionsOrTypeThresholdOrTypeSqlBuilder().
172+
TypeResample(expr.NewTypeResampleBuilder().
173+
RefId(options.RefID).
174+
Expression(options.ResampleExpression.Expression).
175+
IntervalMs(*options.IntervalMs).
176+
MaxDataPoints(*options.MaxDataPoints).
177+
Window(window).
178+
Downsampler(options.ResampleExpression.DownSampler).
179+
Upsampler(options.ResampleExpression.UpSampler),
180+
))
171181
}
172182

173183
if options.ThresholdExpression != nil {
174-
res.Model(expr.NewTypeThresholdBuilder().
175-
RefId(options.RefID).
176-
Expression(options.ThresholdExpression.Expression).
177-
IntervalMs(*options.IntervalMs).
178-
MaxDataPoints(*options.MaxDataPoints).
179-
Conditions(newThresholdConditionsOptions(options.ThresholdExpression.ThresholdConditionsOptions)),
180-
)
184+
res.Model(expr.NewTypeMathOrTypeReduceOrTypeResampleOrTypeClassicConditionsOrTypeThresholdOrTypeSqlBuilder().
185+
TypeThreshold(expr.NewTypeThresholdBuilder().
186+
RefId(options.RefID).
187+
Expression(options.ThresholdExpression.Expression).
188+
IntervalMs(*options.IntervalMs).
189+
MaxDataPoints(*options.MaxDataPoints).
190+
Conditions(newThresholdConditionsOptions(options.ThresholdExpression.ThresholdConditionsOptions)),
191+
))
181192
}
182193

183194
return res

observability-lib/grafana/builder_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package grafana_test
22

33
import (
4+
"encoding/json"
45
"testing"
56

67
"github.com/grafana/grafana-foundation-sdk/go/alerting"
@@ -481,6 +482,81 @@ func TestBuilder_AddPanel(t *testing.T) {
481482
})
482483
}
483484

485+
func TestNewAlertRule_ConditionExpressionModels(t *testing.T) {
486+
t.Run("builds a model for every expression type", func(t *testing.T) {
487+
ruleBuilder := grafana.NewAlertRule(&grafana.AlertOptions{
488+
Title: "Expression Alert",
489+
Query: []grafana.RuleQuery{
490+
{
491+
Expr: `my_metric`,
492+
Instant: true,
493+
RefID: "A",
494+
Datasource: "datasource-uid",
495+
},
496+
},
497+
QueryRefCondition: "E",
498+
Condition: []grafana.ConditionQuery{
499+
{
500+
RefID: "B",
501+
ReduceExpression: &grafana.ReduceExpression{
502+
Expression: "A",
503+
Reducer: expr.TypeReduceReducerSum,
504+
},
505+
},
506+
{
507+
RefID: "C",
508+
MathExpression: &grafana.MathExpression{
509+
Expression: "$B * 2",
510+
},
511+
},
512+
{
513+
RefID: "D",
514+
ResampleExpression: &grafana.ResampleExpression{
515+
Expression: "A",
516+
Window: "10s",
517+
DownSampler: expr.TypeResampleDownsamplerMean,
518+
UpSampler: expr.TypeResampleUpsamplerPad,
519+
},
520+
},
521+
{
522+
RefID: "E",
523+
ThresholdExpression: &grafana.ThresholdExpression{
524+
Expression: "D",
525+
ThresholdConditionsOptions: grafana.ThresholdConditionsOption{
526+
Params: []float64{2},
527+
Type: expr.ExprTypeThresholdConditionsEvaluatorTypeLt,
528+
},
529+
},
530+
},
531+
},
532+
})
533+
534+
rule, err := ruleBuilder.Build()
535+
require.NoError(t, err)
536+
537+
models := map[string]string{}
538+
for _, query := range rule.Data {
539+
require.NotNil(t, query.RefId)
540+
raw, errMarshal := json.Marshal(query.Model)
541+
require.NoError(t, errMarshal)
542+
models[*query.RefId] = string(raw)
543+
}
544+
545+
require.Contains(t, models["B"], `"type":"reduce"`)
546+
547+
require.Contains(t, models["C"], `"type":"math"`)
548+
require.Contains(t, models["C"], `"expression":"$B * 2"`)
549+
550+
require.Contains(t, models["D"], `"type":"resample"`)
551+
require.Contains(t, models["D"], `"downsampler":"mean"`)
552+
require.Contains(t, models["D"], `"upsampler":"pad"`)
553+
require.Contains(t, models["D"], `"expression":"A"`)
554+
require.Contains(t, models["D"], `"window":"10s"`)
555+
556+
require.Contains(t, models["E"], `"type":"threshold"`)
557+
})
558+
}
559+
484560
func TestBuilder_AddTimeSeriesPanelWithAlert(t *testing.T) {
485561
t.Run("AddPanel adds a panel to the dashboard", func(t *testing.T) {
486562
builder := grafana.NewBuilder(&grafana.BuilderOptions{

pkg/assets/link.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ func (l *Link) UnmarshalJSON(data []byte) error {
123123
// UnmarshalText implements the encoding.TextUnmarshaler interface.
124124
func (l *Link) UnmarshalText(text []byte) error {
125125
s := string(text)
126-
if strings.HasSuffix(s, "link") {
127-
s = strings.TrimSuffix(s, "link")
126+
if before, ok := strings.CutSuffix(s, "link"); ok {
127+
s = before
128128
s = strings.TrimSuffix(s, " ")
129129
d, err := decimal.NewFromString(s)
130130
if err != nil {
@@ -138,8 +138,8 @@ func (l *Link) UnmarshalText(text []byte) error {
138138
l.Set((*Link)(d.Rat().Num()))
139139
return nil
140140
}
141-
if strings.HasSuffix(s, "juels") {
142-
s = strings.TrimSuffix(s, "juels")
141+
if before, ok := strings.CutSuffix(s, "juels"); ok {
142+
s = before
143143
s = strings.TrimSuffix(s, " ")
144144
}
145145
if _, ok := l.SetString(s, 10); !ok {

pkg/beholder/batch_emitter_service_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func TestChipIngressBatchEmitterService_Emit(t *testing.T) {
9595
require.NoError(t, err)
9696
require.NoError(t, emitter.Start(t.Context()))
9797

98-
for i := 0; i < 3; i++ {
98+
for range 3 {
9999
err = emitter.Emit(t.Context(), []byte("body"),
100100
beholder.AttrKeyDomain, "platform",
101101
beholder.AttrKeyEntity, "TestEvent",
@@ -190,7 +190,7 @@ func TestChipIngressBatchEmitterService_PublishBatchError(t *testing.T) {
190190
require.NoError(t, err)
191191
require.NoError(t, emitter.Start(t.Context()))
192192

193-
for i := 0; i < 3; i++ {
193+
for range 3 {
194194
err = emitter.Emit(t.Context(), []byte("body"),
195195
beholder.AttrKeyDomain, "platform",
196196
beholder.AttrKeyEntity, "TestEvent",
@@ -401,7 +401,7 @@ func TestChipIngressBatchEmitterService_EmitWithCallback(t *testing.T) {
401401
<-firstCallSignal
402402
time.Sleep(100 * time.Millisecond)
403403

404-
for i := 0; i < 10; i++ {
404+
for range 10 {
405405
_ = emitter.Emit(t.Context(), []byte("filler"),
406406
beholder.AttrKeyDomain, "platform",
407407
beholder.AttrKeyEntity, "TestEvent",

pkg/beholder/client.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -549,11 +549,11 @@ func newMeterProvider(cfg Config, resource *sdkresource.Resource, auth Auth, cre
549549
for _, p := range cfg.MetricProducers {
550550
readerOpts = append(readerOpts, sdkmetric.WithProducer(p))
551551
}
552-
return sdkmetric.NewMeterProvider(
552+
mpOpts := append(cfg.metricOptions(),
553553
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exporter, readerOpts...)),
554554
sdkmetric.WithResource(resource),
555-
sdkmetric.WithView(cfg.MetricViews...),
556-
), nil
555+
)
556+
return sdkmetric.NewMeterProvider(mpOpts...), nil
557557
}
558558

559559
// newLoggerOpts creates options for a logger exporter

pkg/beholder/client_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@ func TestChipIngressClient(t *testing.T) {
785785
t.Run("noop client PublishBatch returns empty response", func(t *testing.T) {
786786
noopClient := &chipingress.NoopClient{}
787787
events := []chipingress.CloudEvent{}
788-
for i := 0; i < 3; i++ {
788+
for i := range 3 {
789789
event, err := chipingress.NewEvent(fmt.Sprintf("domain-%d", i), "test.type", []byte("test"), nil)
790790
require.NoError(t, err)
791791
events = append(events, event)
@@ -868,7 +868,7 @@ func TestChipIngressClient(t *testing.T) {
868868
mockClient := chipmocks.NewClient(t)
869869

870870
events := []chipingress.CloudEvent{}
871-
for i := 0; i < 2; i++ {
871+
for i := range 2 {
872872
event, err := chipingress.NewEvent(fmt.Sprintf("domain-%d", i), "test.type", []byte("test"), nil)
873873
require.NoError(t, err)
874874
events = append(events, event)
@@ -936,7 +936,7 @@ func TestClient_batchEmitterService(t *testing.T) {
936936
newBatchClient := func(t *testing.T) *beholder.Client {
937937
t.Helper()
938938
client, err := beholder.NewClient(beholder.Config{
939-
OtelExporterGRPCEndpoint: "localhost:4317",
939+
OtelExporterGRPCEndpoint: "localhost:4317",
940940
// Use simple exporter in this lifecycle test to avoid batch flush/shutdown delays.
941941
EmitterBatchProcessor: false,
942942
LogBatchProcessor: false,

pkg/beholder/config.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ type Config struct {
2626
EmitterMaxQueueSize int
2727
// EmitterBatchProcessor controls custom-message export mode:
2828
// true = batched async export; false = immediate per-record export.
29-
EmitterBatchProcessor bool
29+
EmitterBatchProcessor bool
3030

3131
// OTel Trace
3232
TraceSampleRatio float64
@@ -39,7 +39,10 @@ type Config struct {
3939
// OTel Metric
4040
MetricReaderInterval time.Duration
4141
MetricRetryConfig *RetryConfig
42-
MetricViews []metric.View
42+
MetricViews []metric.View
43+
// MetricCardinalityLimit sets the SDK per-instrument attribute-set limit (0 = disabled).
44+
// DefaultConfig uses DefaultMetricCardinalityLimit as a production safety valve for high-cardinality workloads.
45+
MetricCardinalityLimit int
4346
// MetricCompressor sets the gRPC compressor for metrics. Valid values: "gzip" (default), "none".
4447
MetricCompressor string
4548
MetricProducers []metric.Producer // For example, a prometheus bridge
@@ -108,6 +111,9 @@ var defaultRetryConfig = RetryConfig{
108111
const (
109112
defaultPackageName = "beholder"
110113
defaultMaxConcurrentSends = 10
114+
115+
// DefaultMetricCardinalityLimit is the production safety valve for high-cardinality workloads.
116+
DefaultMetricCardinalityLimit = 100000
111117
)
112118

113119
var defaultOtelAttributes = []attribute.KeyValue{
@@ -127,7 +133,7 @@ func DefaultConfig() Config {
127133
EmitterExportInterval: 1 * time.Second,
128134
EmitterMaxQueueSize: 2048,
129135
// Keep batched export enabled by default for throughput.
130-
EmitterBatchProcessor: true,
136+
EmitterBatchProcessor: true,
131137
// OTel message log exporter retry config
132138
LogRetryConfig: defaultRetryConfig.Copy(),
133139
// Trace
@@ -137,8 +143,9 @@ func DefaultConfig() Config {
137143
// OTel trace exporter retry config
138144
TraceRetryConfig: defaultRetryConfig.Copy(),
139145
// Metric
140-
MetricReaderInterval: 1 * time.Second,
141-
MetricCompressor: "gzip",
146+
MetricReaderInterval: 1 * time.Second,
147+
MetricCompressor: "gzip",
148+
MetricCardinalityLimit: DefaultMetricCardinalityLimit,
142149
// OTel metric exporter retry config
143150
MetricRetryConfig: defaultRetryConfig.Copy(),
144151
// Log
@@ -148,8 +155,8 @@ func DefaultConfig() Config {
148155
LogMaxQueueSize: 2048,
149156
LogBatchProcessor: true,
150157
LogStreamingEnabled: true, // Enable logs streaming by default
151-
LogLevel: zapcore.InfoLevel,
152-
LogCompressor: "gzip",
158+
LogLevel: zapcore.InfoLevel,
159+
LogCompressor: "gzip",
153160
// Chip Ingress Batch Emitter
154161
ChipIngressBatchEmitterEnabled: false,
155162
ChipIngressBufferSize: 1000,
@@ -173,6 +180,7 @@ func TestDefaultConfig() Config {
173180
config.LogRetryConfig.MaxElapsedTime = 0 // Retry is disabled
174181
config.TraceRetryConfig.MaxElapsedTime = 0 // Retry is disabled
175182
config.MetricRetryConfig.MaxElapsedTime = 0 // Retry is disabled
183+
config.MetricCardinalityLimit = 0 // Disable overflow aggregation in unit tests
176184
// Auth disabled for testing (TTL=0 means static auth mode)
177185
config.AuthHeadersTTL = 0
178186
return config
@@ -186,6 +194,7 @@ func TestDefaultConfigHTTPClient() Config {
186194
config.LogBatchProcessor = false
187195
config.OtelExporterGRPCEndpoint = ""
188196
config.OtelExporterHTTPEndpoint = "localhost:4318"
197+
config.MetricCardinalityLimit = 0
189198
// Auth disabled for testing (TTL=0 means static auth mode)
190199
config.AuthHeadersTTL = 0
191200
return config

pkg/beholder/config_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ func TestConfig(t *testing.T) {
5353
// OTel trace exporter retry config
5454
TraceRetryConfig: nil,
5555
// Metric
56-
MetricReaderInterval: 1 * time.Second,
57-
MetricCompressor: "gzip",
56+
MetricReaderInterval: 1 * time.Second,
57+
MetricCompressor: "gzip",
58+
MetricCardinalityLimit: 0,
5859
// OTel metric exporter retry config
5960
MetricRetryConfig: nil,
6061
// Log

0 commit comments

Comments
 (0)