281 lines
8.1 KiB
Go
281 lines
8.1 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package spanprocessor
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"go.opentelemetry.io/collector/component"
|
|
"go.opentelemetry.io/collector/component/componenterror"
|
|
"go.opentelemetry.io/collector/consumer"
|
|
"go.opentelemetry.io/collector/consumer/pdata"
|
|
"go.opentelemetry.io/collector/internal/processor/filterspan"
|
|
"go.opentelemetry.io/collector/processor"
|
|
)
|
|
|
|
type spanProcessor struct {
|
|
nextConsumer consumer.TraceConsumer
|
|
config Config
|
|
toAttributeRules []toAttributeRule
|
|
include filterspan.Matcher
|
|
exclude filterspan.Matcher
|
|
}
|
|
|
|
var _ component.TraceProcessor = (*spanProcessor)(nil)
|
|
|
|
// toAttributeRule is the compiled equivalent of config.ToAttributes field.
|
|
type toAttributeRule struct {
|
|
// Compiled regexp.
|
|
re *regexp.Regexp
|
|
|
|
// Attribute names extracted from the regexp's subexpressions.
|
|
attrNames []string
|
|
}
|
|
|
|
// newSpanProcessor returns the span processor.
|
|
func newSpanProcessor(nextConsumer consumer.TraceConsumer, config Config) (*spanProcessor, error) {
|
|
if nextConsumer == nil {
|
|
return nil, componenterror.ErrNilNextConsumer
|
|
}
|
|
|
|
include, err := filterspan.NewMatcher(config.Include)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
exclude, err := filterspan.NewMatcher(config.Exclude)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sp := &spanProcessor{
|
|
nextConsumer: nextConsumer,
|
|
config: config,
|
|
include: include,
|
|
exclude: exclude,
|
|
}
|
|
|
|
// Compile ToAttributes regexp and extract attributes names.
|
|
if config.Rename.ToAttributes != nil {
|
|
for _, pattern := range config.Rename.ToAttributes.Rules {
|
|
re, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid regexp pattern %s", pattern)
|
|
}
|
|
|
|
rule := toAttributeRule{
|
|
re: re,
|
|
// Subexpression names will become attribute names during extraction.
|
|
attrNames: re.SubexpNames(),
|
|
}
|
|
|
|
sp.toAttributeRules = append(sp.toAttributeRules, rule)
|
|
}
|
|
}
|
|
|
|
return sp, nil
|
|
}
|
|
|
|
func (sp *spanProcessor) ConsumeTraces(ctx context.Context, td pdata.Traces) error {
|
|
rss := td.ResourceSpans()
|
|
for i := 0; i < rss.Len(); i++ {
|
|
rs := rss.At(i)
|
|
if rs.IsNil() {
|
|
continue
|
|
}
|
|
serviceName := processor.ServiceNameForResource(rs.Resource())
|
|
ilss := rss.At(i).InstrumentationLibrarySpans()
|
|
for j := 0; j < ilss.Len(); j++ {
|
|
ils := ilss.At(j)
|
|
if ils.IsNil() {
|
|
continue
|
|
}
|
|
spans := ils.Spans()
|
|
for k := 0; k < spans.Len(); k++ {
|
|
s := spans.At(k)
|
|
if s.IsNil() {
|
|
continue
|
|
}
|
|
|
|
if sp.skipSpan(s, serviceName) {
|
|
continue
|
|
}
|
|
sp.processFromAttributes(s)
|
|
sp.processToAttributes(s)
|
|
}
|
|
}
|
|
}
|
|
return sp.nextConsumer.ConsumeTraces(ctx, td)
|
|
}
|
|
|
|
func (sp *spanProcessor) GetCapabilities() component.ProcessorCapabilities {
|
|
return component.ProcessorCapabilities{MutatesConsumedData: true}
|
|
}
|
|
|
|
// Start is invoked during service startup.
|
|
func (sp *spanProcessor) Start(_ context.Context, _ component.Host) error {
|
|
return nil
|
|
}
|
|
|
|
// Shutdown is invoked during service shutdown.
|
|
func (sp *spanProcessor) Shutdown(context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (sp *spanProcessor) processFromAttributes(span pdata.Span) {
|
|
if len(sp.config.Rename.FromAttributes) == 0 {
|
|
// There is FromAttributes rule.
|
|
return
|
|
}
|
|
|
|
attrs := span.Attributes()
|
|
if attrs.Len() == 0 {
|
|
// There are no attributes to create span name from.
|
|
return
|
|
}
|
|
|
|
// Note: There was a separate proposal for creating the string.
|
|
// With benchmarking, strings.Builder is faster than the proposal.
|
|
// For full context, refer to this PR comment:
|
|
// https://go.opentelemetry.io/collector/pull/301#discussion_r318357678
|
|
var sb strings.Builder
|
|
for i, key := range sp.config.Rename.FromAttributes {
|
|
attr, found := attrs.Get(key)
|
|
|
|
// If one of the keys isn't found, the span name is not updated.
|
|
if !found {
|
|
return
|
|
}
|
|
|
|
// Note: WriteString() always return a nil error so there is no error checking
|
|
// for this method call.
|
|
// https://golang.org/src/strings/builder.go?s=3425:3477#L110
|
|
|
|
// Include the separator before appending an attribute value if:
|
|
// this isn't the first value(ie i == 0) loop through the FromAttributes
|
|
// and
|
|
// the separator isn't an empty string.
|
|
if i > 0 && sp.config.Rename.Separator != "" {
|
|
sb.WriteString(sp.config.Rename.Separator)
|
|
}
|
|
|
|
switch attr.Type() {
|
|
case pdata.AttributeValueSTRING:
|
|
sb.WriteString(attr.StringVal())
|
|
case pdata.AttributeValueBOOL:
|
|
sb.WriteString(strconv.FormatBool(attr.BoolVal()))
|
|
case pdata.AttributeValueDOUBLE:
|
|
sb.WriteString(strconv.FormatFloat(attr.DoubleVal(), 'f', -1, 64))
|
|
case pdata.AttributeValueINT:
|
|
sb.WriteString(strconv.FormatInt(attr.IntVal(), 10))
|
|
default:
|
|
sb.WriteString("<unknown-attribute-type>")
|
|
}
|
|
}
|
|
span.SetName(sb.String())
|
|
}
|
|
|
|
func (sp *spanProcessor) processToAttributes(span pdata.Span) {
|
|
if span.Name() == "" {
|
|
// There is no span name to work on.
|
|
return
|
|
}
|
|
|
|
if sp.config.Rename.ToAttributes == nil {
|
|
// No rules to apply.
|
|
return
|
|
}
|
|
|
|
// Process rules one by one. Store results of processing in the span
|
|
// so that each subsequent rule works on the span name that is the output
|
|
// after processing the previous rule.
|
|
for _, rule := range sp.toAttributeRules {
|
|
re := rule.re
|
|
oldName := span.Name()
|
|
|
|
// Match the regular expression and extract matched subexpressions.
|
|
submatches := re.FindStringSubmatch(oldName)
|
|
if submatches == nil {
|
|
continue
|
|
}
|
|
// There is a match. We will also need positions of subexpression matches.
|
|
submatchIdxPairs := re.FindStringSubmatchIndex(oldName)
|
|
|
|
// A place to accumulate new span name.
|
|
var sb strings.Builder
|
|
|
|
// Index in the oldName until which we traversed.
|
|
var oldNameIndex = 0
|
|
|
|
attrs := span.Attributes()
|
|
|
|
// TODO: Pre-allocate len(submatches) space in the attributes.
|
|
|
|
// Start from index 1, which is the first submatch (index 0 is the entire match).
|
|
// We will go over submatches and will simultaneously build a new span name,
|
|
// replacing matched subexpressions by attribute names.
|
|
for i := 1; i < len(submatches); i++ {
|
|
attrs.UpsertString(rule.attrNames[i], submatches[i])
|
|
|
|
// Add part of span name from end of previous match to start of this match
|
|
// and then add attribute name wrapped in curly brackets.
|
|
matchStartIndex := submatchIdxPairs[i*2] // start of i'th submatch.
|
|
sb.WriteString(oldName[oldNameIndex:matchStartIndex] + "{" + rule.attrNames[i] + "}")
|
|
|
|
// Advance the index to the end of current match.
|
|
oldNameIndex = submatchIdxPairs[i*2+1] // end of i'th submatch.
|
|
}
|
|
if oldNameIndex < len(oldName) {
|
|
// Append the remainder, from the end of last match until end of span name.
|
|
sb.WriteString(oldName[oldNameIndex:])
|
|
}
|
|
|
|
// Set new span name.
|
|
span.SetName(sb.String())
|
|
|
|
if sp.config.Rename.ToAttributes.BreakAfterMatch {
|
|
// Stop processing, break after first match is requested.
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// skipSpan determines if a span should be processed.
|
|
// True is returned when a span should be skipped.
|
|
// False is returned when a span should not be skipped.
|
|
// The logic determining if a span should be processed is set
|
|
// in the attribute configuration with the include and exclude settings.
|
|
// Include properties are checked before exclude settings are checked.
|
|
func (sp *spanProcessor) skipSpan(span pdata.Span, serviceName string) bool {
|
|
if sp.include != nil {
|
|
// A false returned in this case means the span should not be processed.
|
|
if include := sp.include.MatchSpan(span, serviceName); !include {
|
|
return true
|
|
}
|
|
}
|
|
|
|
if sp.exclude != nil {
|
|
// A true returned in this case means the span should not be processed.
|
|
if exclude := sp.exclude.MatchSpan(span, serviceName); exclude {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|