Add processor duration metric (#13227)

#### Description

Add duration metric to processors.

#### Testing

Unit tests added.

#### Documentation

Implements issue [Processor duration metric
#13231](https://github.com/open-telemetry/opentelemetry-collector/issues/13231).

Basic documentation generated by mdatagen.

---------

Co-authored-by: Joshua MacDonald <jmacd@users.noreply.github.com>
Co-authored-by: Pablo Baeyens <pablo.baeyens@datadoghq.com>
This commit is contained in:
Andres Borja 2025-08-07 08:50:03 -07:00 committed by GitHub
parent 4c24b49532
commit e8497dfbf6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 183 additions and 5 deletions

View File

@ -0,0 +1,25 @@
# Use this changelog template to create an entry for release notes.
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement
# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
component: processorhelper
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add processor internal duration metric.
# One or more tracking issues or pull requests related to the change
issues: [13231]
# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [user]

View File

@ -14,6 +14,14 @@ Number of items passed to the processor. [alpha]
| ---- | ----------- | ---------- | --------- | | ---- | ----------- | ---------- | --------- |
| {items} | Sum | Int | true | | {items} | Sum | Int | true |
### otelcol_processor_internal_duration
Duration of time taken to process a batch of telemetry data through the processor. [alpha]
| Unit | Metric Type | Value Type |
| ---- | ----------- | ---------- |
| s | Histogram | Double |
### otelcol_processor_outgoing_items ### otelcol_processor_outgoing_items
Number of items emitted from the processor. [alpha] Number of items emitted from the processor. [alpha]

View File

@ -23,11 +23,12 @@ func Tracer(settings component.TelemetrySettings) trace.Tracer {
// TelemetryBuilder provides an interface for components to report telemetry // TelemetryBuilder provides an interface for components to report telemetry
// as defined in metadata and user config. // as defined in metadata and user config.
type TelemetryBuilder struct { type TelemetryBuilder struct {
meter metric.Meter meter metric.Meter
mu sync.Mutex mu sync.Mutex
registrations []metric.Registration registrations []metric.Registration
ProcessorIncomingItems metric.Int64Counter ProcessorIncomingItems metric.Int64Counter
ProcessorOutgoingItems metric.Int64Counter ProcessorInternalDuration metric.Float64Histogram
ProcessorOutgoingItems metric.Int64Counter
} }
// TelemetryBuilderOption applies changes to default builder. // TelemetryBuilderOption applies changes to default builder.
@ -65,6 +66,12 @@ func NewTelemetryBuilder(settings component.TelemetrySettings, options ...Teleme
metric.WithUnit("{items}"), metric.WithUnit("{items}"),
) )
errs = errors.Join(errs, err) errs = errors.Join(errs, err)
builder.ProcessorInternalDuration, err = builder.meter.Float64Histogram(
"otelcol_processor_internal_duration",
metric.WithDescription("Duration of time taken to process a batch of telemetry data through the processor. [alpha]"),
metric.WithUnit("s"),
)
errs = errors.Join(errs, err)
builder.ProcessorOutgoingItems, err = builder.meter.Int64Counter( builder.ProcessorOutgoingItems, err = builder.meter.Int64Counter(
"otelcol_processor_outgoing_items", "otelcol_processor_outgoing_items",
metric.WithDescription("Number of items emitted from the processor. [alpha]"), metric.WithDescription("Number of items emitted from the processor. [alpha]"),

View File

@ -28,6 +28,21 @@ func AssertEqualProcessorIncomingItems(t *testing.T, tt *componenttest.Telemetry
metricdatatest.AssertEqual(t, want, got, opts...) metricdatatest.AssertEqual(t, want, got, opts...)
} }
func AssertEqualProcessorInternalDuration(t *testing.T, tt *componenttest.Telemetry, dps []metricdata.HistogramDataPoint[float64], opts ...metricdatatest.Option) {
want := metricdata.Metrics{
Name: "otelcol_processor_internal_duration",
Description: "Duration of time taken to process a batch of telemetry data through the processor. [alpha]",
Unit: "s",
Data: metricdata.Histogram[float64]{
Temporality: metricdata.CumulativeTemporality,
DataPoints: dps,
},
}
got, err := tt.GetMetric("otelcol_processor_internal_duration")
require.NoError(t, err)
metricdatatest.AssertEqual(t, want, got, opts...)
}
func AssertEqualProcessorOutgoingItems(t *testing.T, tt *componenttest.Telemetry, dps []metricdata.DataPoint[int64], opts ...metricdatatest.Option) { func AssertEqualProcessorOutgoingItems(t *testing.T, tt *componenttest.Telemetry, dps []metricdata.DataPoint[int64], opts ...metricdatatest.Option) {
want := metricdata.Metrics{ want := metricdata.Metrics{
Name: "otelcol_processor_outgoing_items", Name: "otelcol_processor_outgoing_items",

View File

@ -20,10 +20,14 @@ func TestSetupTelemetry(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer tb.Shutdown() defer tb.Shutdown()
tb.ProcessorIncomingItems.Add(context.Background(), 1) tb.ProcessorIncomingItems.Add(context.Background(), 1)
tb.ProcessorInternalDuration.Record(context.Background(), 1)
tb.ProcessorOutgoingItems.Add(context.Background(), 1) tb.ProcessorOutgoingItems.Add(context.Background(), 1)
AssertEqualProcessorIncomingItems(t, testTel, AssertEqualProcessorIncomingItems(t, testTel,
[]metricdata.DataPoint[int64]{{Value: 1}}, []metricdata.DataPoint[int64]{{Value: 1}},
metricdatatest.IgnoreTimestamp()) metricdatatest.IgnoreTimestamp())
AssertEqualProcessorInternalDuration(t, testTel,
[]metricdata.HistogramDataPoint[float64]{{}}, metricdatatest.IgnoreValue(),
metricdatatest.IgnoreTimestamp())
AssertEqualProcessorOutgoingItems(t, testTel, AssertEqualProcessorOutgoingItems(t, testTel,
[]metricdata.DataPoint[int64]{{Value: 1}}, []metricdata.DataPoint[int64]{{Value: 1}},
metricdatatest.IgnoreTimestamp()) metricdatatest.IgnoreTimestamp())

View File

@ -6,6 +6,7 @@ package processorhelper // import "go.opentelemetry.io/collector/processor/proce
import ( import (
"context" "context"
"errors" "errors"
"time"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -49,10 +50,14 @@ func NewLogs(
logsConsumer, err := consumer.NewLogs(func(ctx context.Context, ld plog.Logs) error { logsConsumer, err := consumer.NewLogs(func(ctx context.Context, ld plog.Logs) error {
span := trace.SpanFromContext(ctx) span := trace.SpanFromContext(ctx)
span.AddEvent("Start processing.", eventOptions) span.AddEvent("Start processing.", eventOptions)
startTime := time.Now()
recordsIn := ld.LogRecordCount() recordsIn := ld.LogRecordCount()
var errFunc error var errFunc error
ld, errFunc = logsFunc(ctx, ld) ld, errFunc = logsFunc(ctx, ld)
obs.recordInternalDuration(ctx, startTime)
span.AddEvent("End processing.", eventOptions) span.AddEvent("End processing.", eventOptions)
if errFunc != nil { if errFunc != nil {
obs.recordInOut(ctx, recordsIn, 0) obs.recordInOut(ctx, recordsIn, 0)

View File

@ -183,6 +183,33 @@ func TestLogs_RecordIn_ErrorOut(t *testing.T) {
}, metricdatatest.IgnoreTimestamp()) }, metricdatatest.IgnoreTimestamp())
} }
func TestLogs_ProcessInternalDuration(t *testing.T) {
mockAggregate := func(_ context.Context, _ plog.Logs) (plog.Logs, error) {
ld := plog.NewLogs()
ld.ResourceLogs().AppendEmpty().ScopeLogs().AppendEmpty().LogRecords().AppendEmpty()
return ld, nil
}
incomingLogs := plog.NewLogs()
tel := componenttest.NewTelemetry()
lp, err := NewLogs(context.Background(), newSettings(tel), &testLogsCfg, consumertest.NewNop(), mockAggregate)
require.NoError(t, err)
assert.NoError(t, lp.Start(context.Background(), componenttest.NewNopHost()))
assert.NoError(t, lp.ConsumeLogs(context.Background(), incomingLogs))
assert.NoError(t, lp.Shutdown(context.Background()))
metadatatest.AssertEqualProcessorInternalDuration(t, tel,
[]metricdata.HistogramDataPoint[float64]{
{
Count: 1,
BucketCounts: []uint64{1},
Attributes: attribute.NewSet(attribute.String("processor", "processorhelper"), attribute.String("otel.signal", "logs")),
},
}, metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue())
}
func newSettings(tel *componenttest.Telemetry) processor.Settings { func newSettings(tel *componenttest.Telemetry) processor.Settings {
set := processortest.NewNopSettings(component.MustNewType("processorhelper")) set := processortest.NewNopSettings(component.MustNewType("processorhelper"))
set.TelemetrySettings = tel.NewTelemetrySettings() set.TelemetrySettings = tel.NewTelemetrySettings()

View File

@ -28,3 +28,13 @@ telemetry:
sum: sum:
value_type: int value_type: int
monotonic: true monotonic: true
processor_internal_duration:
enabled: true
stability:
level: alpha
description: Duration of time taken to process a batch of telemetry data through the processor.
unit: s
histogram:
async: false
value_type: double

View File

@ -6,6 +6,7 @@ package processorhelper // import "go.opentelemetry.io/collector/processor/proce
import ( import (
"context" "context"
"errors" "errors"
"time"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -49,10 +50,14 @@ func NewMetrics(
metricsConsumer, err := consumer.NewMetrics(func(ctx context.Context, md pmetric.Metrics) error { metricsConsumer, err := consumer.NewMetrics(func(ctx context.Context, md pmetric.Metrics) error {
span := trace.SpanFromContext(ctx) span := trace.SpanFromContext(ctx)
span.AddEvent("Start processing.", eventOptions) span.AddEvent("Start processing.", eventOptions)
startTime := time.Now()
pointsIn := md.DataPointCount() pointsIn := md.DataPointCount()
var errFunc error var errFunc error
md, errFunc = metricsFunc(ctx, md) md, errFunc = metricsFunc(ctx, md)
obs.recordInternalDuration(ctx, startTime)
span.AddEvent("End processing.", eventOptions) span.AddEvent("End processing.", eventOptions)
if errFunc != nil { if errFunc != nil {
obs.recordInOut(ctx, pointsIn, 0) obs.recordInOut(ctx, pointsIn, 0)

View File

@ -180,3 +180,32 @@ func TestMetrics_RecordIn_ErrorOut(t *testing.T) {
}, },
}, metricdatatest.IgnoreTimestamp()) }, metricdatatest.IgnoreTimestamp())
} }
func TestMetrics_ProcessInternalDuration(t *testing.T) {
mockAggregate := func(_ context.Context, _ pmetric.Metrics) (pmetric.Metrics, error) {
md := pmetric.NewMetrics()
md.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty().SetEmptySum().DataPoints().AppendEmpty()
md.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty().SetEmptySum().DataPoints().AppendEmpty()
md.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty().SetEmptySum().DataPoints().AppendEmpty()
return md, nil
}
incomingMetrics := pmetric.NewMetrics()
tel := componenttest.NewTelemetry()
mp, err := NewMetrics(context.Background(), newSettings(tel), &testMetricsCfg, consumertest.NewNop(), mockAggregate)
require.NoError(t, err)
assert.NoError(t, mp.Start(context.Background(), componenttest.NewNopHost()))
assert.NoError(t, mp.ConsumeMetrics(context.Background(), incomingMetrics))
assert.NoError(t, mp.Shutdown(context.Background()))
metadatatest.AssertEqualProcessorInternalDuration(t, tel,
[]metricdata.HistogramDataPoint[float64]{
{
Count: 1,
BucketCounts: []uint64{1},
Attributes: attribute.NewSet(attribute.String("processor", "processorhelper"), attribute.String("otel.signal", "metrics")),
},
}, metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue())
}

View File

@ -5,6 +5,7 @@ package processorhelper // import "go.opentelemetry.io/collector/processor/proce
import ( import (
"context" "context"
"time"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric"
@ -40,3 +41,8 @@ func (or *obsReport) recordInOut(ctx context.Context, incoming, outgoing int) {
or.telemetryBuilder.ProcessorIncomingItems.Add(ctx, int64(incoming), or.otelAttrs) or.telemetryBuilder.ProcessorIncomingItems.Add(ctx, int64(incoming), or.otelAttrs)
or.telemetryBuilder.ProcessorOutgoingItems.Add(ctx, int64(outgoing), or.otelAttrs) or.telemetryBuilder.ProcessorOutgoingItems.Add(ctx, int64(outgoing), or.otelAttrs)
} }
func (or *obsReport) recordInternalDuration(ctx context.Context, startTime time.Time) {
duration := time.Since(startTime)
or.telemetryBuilder.ProcessorInternalDuration.Record(ctx, duration.Seconds(), or.otelAttrs)
}

View File

@ -6,6 +6,7 @@ package processorhelper // import "go.opentelemetry.io/collector/processor/proce
import ( import (
"context" "context"
"errors" "errors"
"time"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -49,10 +50,14 @@ func NewTraces(
traceConsumer, err := consumer.NewTraces(func(ctx context.Context, td ptrace.Traces) error { traceConsumer, err := consumer.NewTraces(func(ctx context.Context, td ptrace.Traces) error {
span := trace.SpanFromContext(ctx) span := trace.SpanFromContext(ctx)
span.AddEvent("Start processing.", eventOptions) span.AddEvent("Start processing.", eventOptions)
startTime := time.Now()
spansIn := td.SpanCount() spansIn := td.SpanCount()
var errFunc error var errFunc error
td, errFunc = tracesFunc(ctx, td) td, errFunc = tracesFunc(ctx, td)
obs.recordInternalDuration(ctx, startTime)
span.AddEvent("End processing.", eventOptions) span.AddEvent("End processing.", eventOptions)
if errFunc != nil { if errFunc != nil {
obs.recordInOut(ctx, spansIn, 0) obs.recordInOut(ctx, spansIn, 0)

View File

@ -184,3 +184,30 @@ func TestTraces_RecordIn_ErrorOut(t *testing.T) {
}, },
}, metricdatatest.IgnoreTimestamp()) }, metricdatatest.IgnoreTimestamp())
} }
func TestTraces_ProcessInternalDuration(t *testing.T) {
mockAggregate := func(_ context.Context, _ ptrace.Traces) (ptrace.Traces, error) {
td := ptrace.NewTraces()
td.ResourceSpans().AppendEmpty().ScopeSpans().AppendEmpty().Spans().AppendEmpty()
return td, nil
}
incomingTraces := ptrace.NewTraces()
tel := componenttest.NewTelemetry()
tp, err := NewTraces(context.Background(), newSettings(tel), &testLogsCfg, consumertest.NewNop(), mockAggregate)
require.NoError(t, err)
assert.NoError(t, tp.Start(context.Background(), componenttest.NewNopHost()))
assert.NoError(t, tp.ConsumeTraces(context.Background(), incomingTraces))
assert.NoError(t, tp.Shutdown(context.Background()))
metadatatest.AssertEqualProcessorInternalDuration(t, tel,
[]metricdata.HistogramDataPoint[float64]{
{
Count: 1,
BucketCounts: []uint64{1},
Attributes: attribute.NewSet(attribute.String("processor", "processorhelper"), attribute.String("otel.signal", "traces")),
},
}, metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue())
}

View File

@ -379,6 +379,11 @@ func configureViews(level configtelemetry.Level) []config.View {
dropViewOption(&config.ViewSelector{ dropViewOption(&config.ViewSelector{
MeterName: ptr("go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"), MeterName: ptr("go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"),
}), }),
// Drop duration metric if the level is not detailed
dropViewOption(&config.ViewSelector{
MeterName: ptr("go.opentelemetry.io/collector/processor/processorhelper"),
InstrumentName: ptr("otelcol_processor_internal_duration"),
}),
) )
} }