Skip to content

Commit 5ac7223

Browse files
andrewkrohefd6
andauthored
feat(filebeat): add input_type resource attribute for CEL OTel telemetry (#50645)
* feat(filebeat): add input_type resource attribute for CEL OTel telemetry CEL emits both custom input metrics and HTTP client metrics via different instrumentation libraries, so they do not share a common filter key by default. Add a built-in input_type resource attribute so both metric families (and CEL traces) can be filtered together consistently across backends that preserve resource attributes as queryable labels/fields. * Apply suggestions from code review Co-authored-by: Dan Kortschak <dan.kortschak@elastic.co> * add logptest import * goimports -local github.com/elastic --------- Co-authored-by: Dan Kortschak <dan.kortschak@elastic.co>
1 parent b82d25a commit 5ac7223

5 files changed

Lines changed: 101 additions & 0 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: enhancement
2+
summary: Add an input_type resource attribute to CEL input OTel metrics for consistent filtering across custom and HTTP client metrics.
3+
component: filebeat

x-pack/filebeat/input/cel/cel_metrics_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/elastic/beats/v7/x-pack/filebeat/otel"
2222
conf "github.com/elastic/elastic-agent-libs/config"
2323
"github.com/elastic/elastic-agent-libs/logp"
24+
"github.com/elastic/elastic-agent-libs/logp/logptest"
2425
"github.com/elastic/elastic-agent-libs/monitoring"
2526

2627
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
@@ -220,6 +221,51 @@ func (e *inMemoryExporter) getMetrics() []metricdata.ResourceMetrics {
220221
return e.metrics
221222
}
222223

224+
func TestCreateOTELMetricsSetsInputTypeResourceAttribute(t *testing.T) {
225+
exporter := &inMemoryExporter{}
226+
otel.GetGlobalMetricsExporterFactory().SetGlobalMetricsExporter(exporter)
227+
defer otel.GetGlobalMetricsExporterFactory().SetGlobalMetricsExporter(nil)
228+
229+
cfg := defaultConfig()
230+
cfg.DataStream = "foo.bar"
231+
cfg.Package = map[string]string{
232+
"name": "foo",
233+
"version": "1.2.3",
234+
}
235+
env := v2.Context{
236+
IDWithoutName: "test_input",
237+
Agent: beat.Info{
238+
Version: "9.0.0",
239+
},
240+
}
241+
242+
ctx := context.Background()
243+
otelMetrics, _, err := createOTELMetrics(ctx, cfg, logptest.NewTestingLogger(t, "cel_metrics_test"), env, http.DefaultTransport, nil)
244+
if err != nil {
245+
t.Fatalf("failed to create OTEL metrics collector: %v", err)
246+
}
247+
defer otelMetrics.Shutdown(ctx)
248+
249+
otelMetrics.StartPeriodic(ctx)
250+
otelMetrics.AddProgramExecutionStarted(ctx, 1)
251+
otelMetrics.EndPeriodic(ctx)
252+
253+
deadline := time.Now().Add(5 * time.Second)
254+
for len(exporter.getMetrics()) == 0 && time.Now().Before(deadline) {
255+
time.Sleep(10 * time.Millisecond)
256+
}
257+
if got := len(exporter.getMetrics()); got == 0 {
258+
t.Fatalf("expected OTEL metrics to be exported, got %d", got)
259+
}
260+
261+
for _, rm := range exporter.getMetrics() {
262+
if rm.Resource != nil && strings.Contains(rm.Resource.String(), "input_type=cel") {
263+
return
264+
}
265+
}
266+
t.Error("expected exported OTEL metrics resource attributes to include input_type=cel")
267+
}
268+
223269
// testPublisher is a publisher that signals when events are published.
224270
type testPublisher struct {
225271
mu sync.Mutex

x-pack/filebeat/input/cel/doc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ These Resource Attributes are included for every CEL input instance:
3636
agent.version version of agent
3737
agent.id the id of the agent
3838
service.instance.id id of the cel input instance
39+
input_type input type name emitting metrics, for example "cel"
3940
package.name name of the integration package
4041
package.version version of the integration package
4142
package.data_stream the datastream name in the integration package

x-pack/filebeat/input/cel/input.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,7 @@ func createOTELMetrics(ctx context.Context, cfg config, log *logp.Logger, env v2
13191319
func getResourceAttributes(env v2.Context, cfg config) []attribute.KeyValue {
13201320
attrs := []attribute.KeyValue{
13211321
semconv.ServiceInstanceID(env.IDWithoutName),
1322+
attribute.String("input_type", inputName),
13221323
attribute.String("package.name", cfg.GetPackageData("name")),
13231324
attribute.String("package.version", cfg.GetPackageData("version")),
13241325
attribute.String("package.data_stream", cfg.DataStream),

x-pack/filebeat/input/cel/input_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
"github.com/google/go-cmp/cmp"
2929
"github.com/icholy/digest"
30+
"go.opentelemetry.io/otel/attribute"
3031

3132
v2 "github.com/elastic/beats/v7/filebeat/input/v2"
3233
inputcursor "github.com/elastic/beats/v7/filebeat/input/v2/input-cursor"
@@ -3250,6 +3251,55 @@ func TestRedactor(t *testing.T) {
32503251
}
32513252
}
32523253

3254+
func TestGetResourceAttributesIncludesInputType(t *testing.T) {
3255+
env := v2.Context{IDWithoutName: "input-id"}
3256+
cfg := config{
3257+
DataStream: "foo.bar",
3258+
Package: map[string]string{
3259+
"name": "foo",
3260+
"version": "1.2.3",
3261+
},
3262+
}
3263+
3264+
attrs := getResourceAttributes(env, cfg)
3265+
attrsMap := toResourceAttributeMap(attrs)
3266+
3267+
if got, want := attrsMap["input_type"], "cel"; got != want {
3268+
t.Errorf("input_type should be set from input name: got %q, want %q", got, want)
3269+
}
3270+
}
3271+
3272+
func TestGetResourceAttributesInputTypeCannotBeOverridden(t *testing.T) {
3273+
t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "input_type=httpjson,deployment.environment=production")
3274+
3275+
env := v2.Context{IDWithoutName: "input-id"}
3276+
cfg := config{
3277+
DataStream: "foo.bar",
3278+
Package: map[string]string{
3279+
"name": "foo",
3280+
"version": "1.2.3",
3281+
},
3282+
}
3283+
3284+
attrs := getResourceAttributes(env, cfg)
3285+
attrsMap := toResourceAttributeMap(attrs)
3286+
3287+
if got, want := attrsMap["input_type"], "cel"; got != want {
3288+
t.Errorf("built-in input_type should not be overridden from OTEL_RESOURCE_ATTRIBUTES: got %q, want %q", got, want)
3289+
}
3290+
if got, want := attrsMap["deployment.environment"], "production"; got != want {
3291+
t.Errorf("custom resource attributes from OTEL_RESOURCE_ATTRIBUTES should still be included: got %q, want %q", got, want)
3292+
}
3293+
}
3294+
3295+
func toResourceAttributeMap(attrs []attribute.KeyValue) map[string]string {
3296+
result := make(map[string]string, len(attrs))
3297+
for _, attr := range attrs {
3298+
result[string(attr.Key)] = attr.Value.AsString()
3299+
}
3300+
return result
3301+
}
3302+
32533303
// sameErrorOrContains reports whether got matches want: both nil, or got's
32543304
// message contains want's message.
32553305
func sameErrorOrContains(got, want error) bool {

0 commit comments

Comments
 (0)