Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add setStatus operation for spanprocessor #5886

Merged
28 changes: 26 additions & 2 deletions processor/spanprocessor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -96,5 +97,28 @@ span/to_attributes:
- ^\/api\/v1\/document\/(?P<documentId>.*)\/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.
12 changes: 12 additions & 0 deletions processor/spanprocessor/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions processor/spanprocessor/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
25 changes: 23 additions & 2 deletions processor/spanprocessor/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
}

mx-psi marked this conversation as resolved.
Show resolved Hide resolved
sp, err := newSpanProcessor(*oCfg)
if err != nil {
return nil, err
Expand Down
17 changes: 17 additions & 0 deletions processor/spanprocessor/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func (sp *spanProcessor) processTraces(_ context.Context, td pdata.Traces) (pdat
}
sp.processFromAttributes(s)
sp.processToAttributes(s)
sp.processUpdateStatus(s)
}
}
}
Expand Down Expand Up @@ -220,3 +221,19 @@ func (sp *spanProcessor) processToAttributes(span pdata.Span) {
}
}
}

func (sp *spanProcessor) processUpdateStatus(span pdata.Span) {
mx-psi marked this conversation as resolved.
Show resolved Hide resolved
cfg := sp.config.SetStatus
if cfg != nil {
if cfg.Code == statusCodeOk {
mx-psi marked this conversation as resolved.
Show resolved Hide resolved
span.Status().SetCode(pdata.StatusCodeOk)
} else if cfg.Code == statusCodeError {
span.Status().SetCode(pdata.StatusCodeError)
} else if cfg.Code == statusCodeUnset {
span.Status().SetCode(pdata.StatusCodeUnset)
}
if len(cfg.Description) > 0 {
span.Status().SetMessage(cfg.Description)
}
}
}
81 changes: 81 additions & 0 deletions processor/spanprocessor/span_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
18 changes: 18 additions & 0 deletions processor/spanprocessor/testdata/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@ processors:
rules:
- "(?P<operation_website>.*?)$"

# 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:

Expand Down