Skip to content

Commit

Permalink
Add setStatus operation for spanprocessor (open-telemetry#5886)
Browse files Browse the repository at this point in the history
* Add setStatus operation for spanprocessor

* Change code to have text format instead of numeric and add another example

* Add "Unset" value and tests

* Update README for Unset value

* Apply suggestions from code review

Co-authored-by: Pablo Baeyens <pbaeyens31+github@gmail.com>

* From review: Move statuses names to constants and fix "Err" => "Error"

* Code review: fixing style

* One more "Err" and fixing comment(s) as suggested from review

* Clear status description for cases different than error

Co-authored-by: Pablo Baeyens <pbaeyens31+github@gmail.com>
  • Loading branch information
2 people authored and jamesmoessis committed Dec 6, 2021
1 parent 9d9270b commit 959f6f4
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 4 deletions.
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
}
}

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) {
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("")
}
}
}
81 changes: 81 additions & 0 deletions processor/spanprocessor/span_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -594,3 +594,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

0 comments on commit 959f6f4

Please sign in to comment.