diff --git a/processor/spanprocessor/README.md b/processor/spanprocessor/README.md index 6954d29c7feb..de2aa8986242 100644 --- a/processor/spanprocessor/README.md +++ b/processor/spanprocessor/README.md @@ -2,14 +2,15 @@ Supported pipeline types: traces -The span processor modifies the span name based on its attributes or extract span attributes from the span name. Please refer to -[config.go](./config.go) for the config spec. +The span processor modifies the span name based on its attributes or extract span attributes from the span name. It also allows +to change span status. Please refer to [config.go](./config.go) for the config spec. It optionally supports the ability to [include/exclude spans](../README.md#includeexclude-spans). The following actions are supported: - `name`: Modify the name of attributes within a span +- `status`: Modify the status of the span ### Name a span @@ -96,5 +97,28 @@ span/to_attributes: - ^\/api\/v1\/document\/(?P.*)\/update$ ``` +### Set status for span + +The following setting is required: + +- `code`: Represents span status. One of the following values "Unset", "Error", "Ok". + +The following setting is allowed only for code "Error": +- `description` + +Example: + +```yaml +# Set status allows to set specific status for a given span. Possible values are +# Ok, Error and Unset as per +# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#set-status +# The description field allows to set a human-readable message for errors. +span/set_status: + status: + code: Error + description: "some error description" +``` + + Refer to [config.yaml](./testdata/config.yaml) for detailed examples on using the processor. diff --git a/processor/spanprocessor/config.go b/processor/spanprocessor/config.go index d6f386576a8e..ce47c51dc83a 100644 --- a/processor/spanprocessor/config.go +++ b/processor/spanprocessor/config.go @@ -35,6 +35,9 @@ type Config struct { // Note: The field name is `Rename` to avoid collision with the Name() method // from config.NamedEntity Rename Name `mapstructure:"name"` + + // SetStatus specifies status which should be set for this span. + SetStatus *Status `mapstructure:"status"` } // Name specifies the attributes to use to re-name a span. @@ -80,6 +83,15 @@ type ToAttributes struct { BreakAfterMatch bool `mapstructure:"break_after_match"` } +type Status struct { + // Code is one of three values "Ok" or "Error" or "Unset". Please check: + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#set-status + Code string `mapstructure:"code"` + + // Description is an optional field documenting Error statuses. + Description string `mapstructure:"description"` +} + var _ config.Processor = (*Config)(nil) // Validate checks if the processor configuration is valid diff --git a/processor/spanprocessor/config_test.go b/processor/spanprocessor/config_test.go index 17e678f7d395..8bfb387e5881 100644 --- a/processor/spanprocessor/config_test.go +++ b/processor/spanprocessor/config_test.go @@ -87,6 +87,31 @@ func TestLoadConfig(t *testing.T) { }, }, }) + + // Set name + p4 := cfg.Processors[config.NewComponentIDWithName("span", "set_status_err")] + assert.Equal(t, p4, &Config{ + ProcessorSettings: config.NewProcessorSettings(config.NewComponentIDWithName("span", "set_status_err")), + SetStatus: &Status{ + Code: "Error", + Description: "some additional error description", + }, + }) + + p5 := cfg.Processors[config.NewComponentIDWithName("span", "set_status_ok")] + assert.Equal(t, p5, &Config{ + ProcessorSettings: config.NewProcessorSettings(config.NewComponentIDWithName("span", "set_status_ok")), + MatchConfig: filterconfig.MatchConfig{ + Include: &filterconfig.MatchProperties{ + Attributes: []filterconfig.Attribute{ + {Key: "http.status_code", Value: 400}, + }, + }, + }, + SetStatus: &Status{ + Code: "Ok", + }, + }) } func createMatchConfig(matchType filterset.MatchType) *filterset.Config { diff --git a/processor/spanprocessor/factory.go b/processor/spanprocessor/factory.go index 2abbfd6a32ae..7c616da02850 100644 --- a/processor/spanprocessor/factory.go +++ b/processor/spanprocessor/factory.go @@ -29,13 +29,24 @@ const ( typeStr = "span" ) +const ( + // status represents span status + statusCodeUnset = "Unset" + statusCodeError = "Error" + statusCodeOk = "Ok" +) + var processorCapabilities = consumer.Capabilities{MutatesData: true} // errMissingRequiredField is returned when a required field in the config // is not specified. // TODO https://github.com/open-telemetry/opentelemetry-collector/issues/215 // Move this to the error package that allows for span name and field to be specified. -var errMissingRequiredField = errors.New("error creating \"span\" processor: either \"from_attributes\" or \"to_attributes\" must be specified in \"name:\"") +var ( + errMissingRequiredField = errors.New("error creating \"span\" processor: either \"from_attributes\" or \"to_attributes\" must be specified in \"name:\" or \"setStatus\" must be specified") + errIncorrectStatusCode = errors.New("error creating \"span\" processor: \"status\" must have specified \"code\" as \"Ok\" or \"Error\" or \"Unset\"") + errIncorrectStatusDescription = errors.New("error creating \"span\" processor: \"description\" can be specified only for \"code\" \"Error\"") +) // NewFactory returns a new factory for the Span processor. func NewFactory() component.ProcessorFactory { @@ -62,10 +73,20 @@ func createTracesProcessor( // processor to be valid. If not set and not enforced, the processor would do no work. oCfg := cfg.(*Config) if len(oCfg.Rename.FromAttributes) == 0 && - (oCfg.Rename.ToAttributes == nil || len(oCfg.Rename.ToAttributes.Rules) == 0) { + (oCfg.Rename.ToAttributes == nil || len(oCfg.Rename.ToAttributes.Rules) == 0) && + oCfg.SetStatus == nil { return nil, errMissingRequiredField } + if oCfg.SetStatus != nil { + if oCfg.SetStatus.Code != statusCodeUnset && oCfg.SetStatus.Code != statusCodeError && oCfg.SetStatus.Code != statusCodeOk { + return nil, errIncorrectStatusCode + } + if len(oCfg.SetStatus.Description) > 0 && oCfg.SetStatus.Code != statusCodeError { + return nil, errIncorrectStatusDescription + } + } + sp, err := newSpanProcessor(*oCfg) if err != nil { return nil, err diff --git a/processor/spanprocessor/span.go b/processor/spanprocessor/span.go index 7cc71619b44e..950d8147366c 100644 --- a/processor/spanprocessor/span.go +++ b/processor/spanprocessor/span.go @@ -97,6 +97,7 @@ func (sp *spanProcessor) processTraces(_ context.Context, td pdata.Traces) (pdat } sp.processFromAttributes(s) sp.processToAttributes(s) + sp.processUpdateStatus(s) } } } @@ -220,3 +221,19 @@ func (sp *spanProcessor) processToAttributes(span pdata.Span) { } } } + +func (sp *spanProcessor) processUpdateStatus(span pdata.Span) { + cfg := sp.config.SetStatus + if cfg != nil { + if cfg.Code == statusCodeOk { + span.Status().SetCode(pdata.StatusCodeOk) + span.Status().SetMessage("") + } else if cfg.Code == statusCodeError { + span.Status().SetCode(pdata.StatusCodeError) + span.Status().SetMessage(cfg.Description) + } else if cfg.Code == statusCodeUnset { + span.Status().SetCode(pdata.StatusCodeUnset) + span.Status().SetMessage("") + } + } +} diff --git a/processor/spanprocessor/span_test.go b/processor/spanprocessor/span_test.go index db850843d7ff..72f4f1cda2b5 100644 --- a/processor/spanprocessor/span_test.go +++ b/processor/spanprocessor/span_test.go @@ -593,3 +593,84 @@ func TestSpanProcessor_skipSpan(t *testing.T) { runIndividualTestCase(t, tc, tp) } } + +func generateTraceDataSetStatus(code pdata.StatusCode, description string, attrs map[string]pdata.AttributeValue) pdata.Traces { + td := pdata.NewTraces() + rs := td.ResourceSpans().AppendEmpty() + span := rs.InstrumentationLibrarySpans().AppendEmpty().Spans().AppendEmpty() + span.Status().SetCode(code) + span.Status().SetMessage(description) + span.Attributes().InitFromMap(attrs).Sort() + return td +} + +func TestSpanProcessor_setStatusCode(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + oCfg := cfg.(*Config) + oCfg.SetStatus = &Status{ + Code: "Error", + Description: "Set custom error message", + } + tp, err := factory.CreateTracesProcessor(context.Background(), componenttest.NewNopProcessorCreateSettings(), oCfg, consumertest.NewNop()) + require.Nil(t, err) + require.NotNil(t, tp) + + td := generateTraceDataSetStatus(pdata.StatusCodeUnset, "foobar", nil) + td.InternalRep() + + assert.NoError(t, tp.ConsumeTraces(context.Background(), td)) + + assert.EqualValues(t, generateTraceDataSetStatus(pdata.StatusCodeError, "Set custom error message", nil), td) +} + +func TestSpanProcessor_setStatusCodeConditionally(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + oCfg := cfg.(*Config) + oCfg.SetStatus = &Status{ + Code: "Error", + Description: "custom error message", + } + // This test numer two include rule for applying rule only for status code 400 + oCfg.Include = &filterconfig.MatchProperties{ + Attributes: []filterconfig.Attribute{ + {Key: "http.status_code", Value: 400}, + }, + } + tp, err := factory.CreateTracesProcessor(context.Background(), componenttest.NewNopProcessorCreateSettings(), oCfg, consumertest.NewNop()) + require.Nil(t, err) + require.NotNil(t, tp) + + testCases := []struct { + inputAttributes map[string]pdata.AttributeValue + inputStatusCode pdata.StatusCode + outputStatusCode pdata.StatusCode + outputStatusDescription string + }{ + { + // without attribiutes - should not apply rule and leave status code as it is + inputStatusCode: pdata.StatusCodeOk, + outputStatusCode: pdata.StatusCodeOk, + }, + { + inputAttributes: map[string]pdata.AttributeValue{ + "http.status_code": pdata.NewAttributeValueInt(400), + }, + inputStatusCode: pdata.StatusCodeOk, + outputStatusCode: pdata.StatusCodeError, + outputStatusDescription: "custom error message", + }, + } + + for _, tc := range testCases { + t.Run("set-status-test", func(t *testing.T) { + td := generateTraceDataSetStatus(tc.inputStatusCode, "", tc.inputAttributes) + td.InternalRep() + + assert.NoError(t, tp.ConsumeTraces(context.Background(), td)) + + assert.EqualValues(t, generateTraceDataSetStatus(tc.outputStatusCode, tc.outputStatusDescription, tc.inputAttributes), td) + }) + } +} diff --git a/processor/spanprocessor/testdata/config.yaml b/processor/spanprocessor/testdata/config.yaml index 59884cd2d3dd..1c900520d32e 100644 --- a/processor/spanprocessor/testdata/config.yaml +++ b/processor/spanprocessor/testdata/config.yaml @@ -84,6 +84,24 @@ processors: rules: - "(?P.*?)$" + # This example changes status of a span to error and sets description. + # Possible values for code are: "Ok", "Error" or "Unset". + # Description is an optional field used for documenting Error statuses. + span/set_status_err: + status: + code: "Error" + description: "some additional error description" + + # However you may want to set status conditionally. Example below sets + # status to success only when attribute http.status_code is equal to 400 + span/set_status_ok: + include: + attributes: + - Key: http.status_code + Value: 400 + status: + code: "Ok" + exporters: nop: