Files

1139 lines
40 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 signozspanmetricsprocessor
import (
"bytes"
"context"
"fmt"
"net/url"
"regexp"
"sort"
"strings"
"sync"
"time"
"unicode"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/consumer"
"go.opentelemetry.io/collector/exporter"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/pmetric"
"go.opentelemetry.io/collector/pdata/ptrace"
conventions "go.opentelemetry.io/collector/semconv/v1.6.1"
"go.uber.org/multierr"
"go.uber.org/zap"
"github.com/SigNoz/signoz-otel-collector/processor/signozspanmetricsprocessor/internal/cache"
)
const (
serviceNameKey = conventions.AttributeServiceName
operationKey = "operation" // OpenTelemetry non-standard constant.
spanKindKey = "span.kind" // OpenTelemetry non-standard constant.
statusCodeKey = "status.code" // OpenTelemetry non-standard constant.
tagHTTPStatusCode = conventions.AttributeHTTPStatusCode
metricKeySeparator = string(byte(0))
traceIDKey = "trace_id"
defaultDimensionsCacheSize = 1000
resourcePrefix = "resource_"
)
var (
defaultLatencyHistogramBucketsMs = []float64{
2, 4, 6, 8, 10, 50, 100, 200, 400, 800, 1000, 1400, 2000, 5000, 10_000, 15_000,
}
)
type exemplarData struct {
traceID pcommon.TraceID
spanID pcommon.SpanID
value float64
}
type metricKey string
type processorImp struct {
lock sync.Mutex
logger *zap.Logger
instanceID string
config Config
metricsExporter exporter.Metrics
nextConsumer consumer.Traces
// Additional dimensions to add to metrics.
dimensions []dimension // signoz_latency metric
callDimensions []dimension // signoz_calls_total metric
dbCallDimensions []dimension // signoz_db_latency_* metric
externalCallDimensions []dimension // signoz_external_call_latency_* metric
// The starting time of the data points.
startTimestamp pcommon.Timestamp
// Histogram.
histograms map[metricKey]*histogramData // signoz_latency metric
latencyBounds []float64
callHistograms map[metricKey]*histogramData // signoz_calls_total metric
callLatencyBounds []float64
dbCallHistograms map[metricKey]*histogramData // signoz_db_latency_* metric
dbCallLatencyBounds []float64
externalCallHistograms map[metricKey]*histogramData // signoz_external_call_latency_* metric
externalCallLatencyBounds []float64
keyBuf *bytes.Buffer
// An LRU cache of dimension key-value maps keyed by a unique identifier formed by a concatenation of its values:
// e.g. { "foo/barOK": { "serviceName": "foo", "operation": "/bar", "status_code": "OK" }}
metricKeyToDimensions *cache.Cache[metricKey, pcommon.Map]
callMetricKeyToDimensions *cache.Cache[metricKey, pcommon.Map]
dbCallMetricKeyToDimensions *cache.Cache[metricKey, pcommon.Map]
externalCallMetricKeyToDimensions *cache.Cache[metricKey, pcommon.Map]
attrsCardinality map[string]map[string]struct{}
excludePatternRegex map[string]*regexp.Regexp
}
type dimension struct {
name string
value *pcommon.Value
}
func newDimensions(cfgDims []Dimension) []dimension {
if len(cfgDims) == 0 {
return nil
}
dims := make([]dimension, len(cfgDims))
for i := range cfgDims {
dims[i].name = cfgDims[i].Name
if cfgDims[i].Default != nil {
val := pcommon.NewValueStr(*cfgDims[i].Default)
dims[i].value = &val
}
}
return dims
}
type histogramData struct {
count uint64
sum float64
bucketCounts []uint64
exemplarsData []exemplarData
}
func newProcessor(logger *zap.Logger, instanceID string, config component.Config, nextConsumer consumer.Traces) (*processorImp, error) {
logger.Info("Building signozspanmetricsprocessor")
pConfig := config.(*Config)
bounds := defaultLatencyHistogramBucketsMs
if pConfig.LatencyHistogramBuckets != nil {
bounds = mapDurationsToMillis(pConfig.LatencyHistogramBuckets)
}
if err := validateDimensions(pConfig.Dimensions, pConfig.skipSanitizeLabel); err != nil {
return nil, err
}
callDimensions := []Dimension{
{Name: tagHTTPStatusCode},
}
callDimensions = append(callDimensions, pConfig.Dimensions...)
dbCallDimensions := []Dimension{
{Name: conventions.AttributeDBSystem},
{Name: conventions.AttributeDBName},
}
dbCallDimensions = append(dbCallDimensions, pConfig.Dimensions...)
var externalCallDimensions = []Dimension{
{Name: tagHTTPStatusCode},
}
externalCallDimensions = append(externalCallDimensions, pConfig.Dimensions...)
if pConfig.DimensionsCacheSize <= 0 {
return nil, fmt.Errorf(
"invalid cache size: %v, the maximum number of the items in the cache should be positive",
pConfig.DimensionsCacheSize,
)
}
metricKeyToDimensionsCache, err := cache.NewCache[metricKey, pcommon.Map](pConfig.DimensionsCacheSize)
if err != nil {
return nil, err
}
callMetricKeyToDimensionsCache, err := cache.NewCache[metricKey, pcommon.Map](pConfig.DimensionsCacheSize)
if err != nil {
return nil, err
}
dbMetricKeyToDimensionsCache, err := cache.NewCache[metricKey, pcommon.Map](pConfig.DimensionsCacheSize)
if err != nil {
return nil, err
}
externalCallMetricKeyToDimensionsCache, err := cache.NewCache[metricKey, pcommon.Map](pConfig.DimensionsCacheSize)
if err != nil {
return nil, err
}
excludePatternRegex := make(map[string]*regexp.Regexp)
for _, pattern := range pConfig.ExcludePatterns {
excludePatternRegex[pattern.Name] = regexp.MustCompile(pattern.Pattern)
}
return &processorImp{
logger: logger,
instanceID: instanceID,
config: *pConfig,
startTimestamp: pcommon.NewTimestampFromTime(time.Now()),
histograms: make(map[metricKey]*histogramData),
callHistograms: make(map[metricKey]*histogramData),
dbCallHistograms: make(map[metricKey]*histogramData),
externalCallHistograms: make(map[metricKey]*histogramData),
latencyBounds: bounds,
callLatencyBounds: bounds,
dbCallLatencyBounds: bounds,
externalCallLatencyBounds: bounds,
nextConsumer: nextConsumer,
dimensions: newDimensions(pConfig.Dimensions),
callDimensions: newDimensions(callDimensions),
dbCallDimensions: newDimensions(dbCallDimensions),
externalCallDimensions: newDimensions(externalCallDimensions),
keyBuf: bytes.NewBuffer(make([]byte, 0, 1024)),
metricKeyToDimensions: metricKeyToDimensionsCache,
callMetricKeyToDimensions: callMetricKeyToDimensionsCache,
dbCallMetricKeyToDimensions: dbMetricKeyToDimensionsCache,
externalCallMetricKeyToDimensions: externalCallMetricKeyToDimensionsCache,
attrsCardinality: make(map[string]map[string]struct{}),
excludePatternRegex: excludePatternRegex,
}, nil
}
// durationToMillis converts the given duration to the number of milliseconds it represents.
// Note that this can return sub-millisecond (i.e. < 1ms) values as well.
func durationToMillis(d time.Duration) float64 {
return float64(d.Nanoseconds()) / float64(time.Millisecond.Nanoseconds())
}
func mapDurationsToMillis(vs []time.Duration) []float64 {
vsm := make([]float64, len(vs))
for i, v := range vs {
vsm[i] = durationToMillis(v)
}
return vsm
}
// validateDimensions checks duplicates for reserved dimensions and additional dimensions. Considering
// the usage of Prometheus related exporters, we also validate the dimensions after sanitization.
func validateDimensions(dimensions []Dimension, skipSanitizeLabel bool) error {
labelNames := make(map[string]struct{})
for _, key := range []string{serviceNameKey, spanKindKey, statusCodeKey} {
labelNames[key] = struct{}{}
labelNames[sanitize(key, skipSanitizeLabel)] = struct{}{}
}
labelNames[operationKey] = struct{}{}
for _, key := range dimensions {
if _, ok := labelNames[key.Name]; ok {
return fmt.Errorf("duplicate dimension name %s", key.Name)
}
labelNames[key.Name] = struct{}{}
sanitizedName := sanitize(key.Name, skipSanitizeLabel)
if sanitizedName == key.Name {
continue
}
if _, ok := labelNames[sanitizedName]; ok {
return fmt.Errorf("duplicate dimension name %s after sanitization", sanitizedName)
}
labelNames[sanitizedName] = struct{}{}
}
return nil
}
func (p *processorImp) shouldSkip(serviceName string, span ptrace.Span, resourceAttrs pcommon.Map) bool {
for key, pattern := range p.excludePatternRegex {
if key == serviceNameKey && pattern.MatchString(serviceName) {
return true
}
if key == operationKey && pattern.MatchString(span.Name()) {
return true
}
if key == spanKindKey && pattern.MatchString(span.Kind().String()) {
return true
}
if key == statusCodeKey && pattern.MatchString(span.Status().Code().String()) {
return true
}
matched := false
span.Attributes().Range(func(k string, v pcommon.Value) bool {
if key == k && pattern.MatchString(v.AsString()) {
matched = true
}
return true
})
resourceAttrs.Range(func(k string, v pcommon.Value) bool {
if key == k && pattern.MatchString(v.AsString()) {
matched = true
}
return true
})
if matched {
return true
}
}
return false
}
// Start implements the component.Component interface.
func (p *processorImp) Start(ctx context.Context, host component.Host) error {
p.logger.Info("Starting signozspanmetricsprocessor with config", zap.Any("config", p.config))
exporters := host.GetExporters()
var availableMetricsExporters []string
// The available list of exporters come from any configured metrics pipelines' exporters.
for k, exp := range exporters[component.DataTypeMetrics] {
metricsExp, ok := exp.(exporter.Metrics)
if !ok {
return fmt.Errorf("the exporter %q isn't a metrics exporter", k.String())
}
availableMetricsExporters = append(availableMetricsExporters, k.String())
p.logger.Debug("Looking for signozspanmetrics exporter from available exporters",
zap.String("signozspanmetrics-exporter", p.config.MetricsExporter),
zap.Any("available-exporters", availableMetricsExporters),
)
if k.String() == p.config.MetricsExporter {
p.metricsExporter = metricsExp
p.logger.Info("Found exporter", zap.String("signozspanmetrics-exporter", p.config.MetricsExporter))
break
}
}
if p.metricsExporter == nil {
return fmt.Errorf("failed to find metrics exporter: '%s'; please configure metrics_exporter from one of: %+v",
p.config.MetricsExporter, availableMetricsExporters)
}
p.logger.Info("Started signozspanmetricsprocessor")
return nil
}
// Shutdown implements the component.Component interface.
func (p *processorImp) Shutdown(ctx context.Context) error {
p.logger.Info("Shutting down signozspanmetricsprocessor")
return nil
}
// Capabilities implements the consumer interface.
func (p *processorImp) Capabilities() consumer.Capabilities {
return consumer.Capabilities{MutatesData: false}
}
// ConsumeTraces implements the consumer.Traces interface.
// It aggregates the trace data to generate metrics, forwarding these metrics to the discovered metrics exporter.
// The original input trace data will be forwarded to the next consumer, unmodified.
func (p *processorImp) ConsumeTraces(ctx context.Context, traces ptrace.Traces) error {
// Forward trace data unmodified and propagate both metrics and trace pipeline errors, if any.
return multierr.Combine(p.tracesToMetrics(ctx, traces), p.nextConsumer.ConsumeTraces(ctx, traces))
}
func (p *processorImp) tracesToMetrics(ctx context.Context, traces ptrace.Traces) error {
p.lock.Lock()
p.aggregateMetrics(traces)
m, err := p.buildMetrics()
// Exemplars are only relevant to this batch of traces, so must be cleared within the lock,
// regardless of error while building metrics, before the next batch of spans is received.
p.resetExemplarData()
// This component no longer needs to read the metrics once built, so it is safe to unlock.
p.lock.Unlock()
if err != nil {
p.logCardinalityInfo()
return err
}
if err = p.metricsExporter.ConsumeMetrics(ctx, m); err != nil {
return err
}
return nil
}
// buildMetrics collects the computed raw metrics data, builds the metrics object and
// writes the raw metrics data into the metrics object.
func (p *processorImp) buildMetrics() (pmetric.Metrics, error) {
m := pmetric.NewMetrics()
ilm := m.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty()
ilm.Scope().SetName("signozspanmetricsprocessor")
if err := p.collectCallMetrics(ilm); err != nil {
return pmetric.Metrics{}, err
}
if err := p.collectLatencyMetrics(ilm); err != nil {
return pmetric.Metrics{}, err
}
if err := p.collectExternalCallMetrics(ilm); err != nil {
return pmetric.Metrics{}, err
}
if err := p.collectDBCallMetrics(ilm); err != nil {
return pmetric.Metrics{}, err
}
p.metricKeyToDimensions.RemoveEvictedItems()
p.callMetricKeyToDimensions.RemoveEvictedItems()
p.dbCallMetricKeyToDimensions.RemoveEvictedItems()
p.externalCallMetricKeyToDimensions.RemoveEvictedItems()
// If delta metrics, reset accumulated data
if p.config.GetAggregationTemporality() == pmetric.AggregationTemporalityDelta {
p.resetAccumulatedMetrics()
}
p.resetExemplarData()
return m, nil
}
func (p *processorImp) logCardinalityInfo() {
for k, v := range p.attrsCardinality {
values := make([]string, 0, len(v))
for key := range v {
values = append(values, key)
}
p.logger.Info("Attribute cardinality", zap.String("attribute", k), zap.Int("cardinality", len(v)))
p.logger.Debug("Attribute values", zap.Strings("values", values))
}
}
// collectLatencyMetrics collects the raw latency metrics, writing the data
// into the given instrumentation library metrics.
func (p *processorImp) collectLatencyMetrics(ilm pmetric.ScopeMetrics) error {
mLatency := ilm.Metrics().AppendEmpty()
mLatency.SetName("signoz_latency")
mLatency.SetUnit("ms")
mLatency.SetEmptyHistogram().SetAggregationTemporality(p.config.GetAggregationTemporality())
dps := mLatency.Histogram().DataPoints()
dps.EnsureCapacity(len(p.histograms))
timestamp := pcommon.NewTimestampFromTime(time.Now())
for key, hist := range p.histograms {
dpLatency := dps.AppendEmpty()
dpLatency.SetStartTimestamp(p.startTimestamp)
dpLatency.SetTimestamp(timestamp)
dpLatency.ExplicitBounds().FromRaw(p.latencyBounds)
dpLatency.BucketCounts().FromRaw(hist.bucketCounts)
dpLatency.SetCount(hist.count)
dpLatency.SetSum(hist.sum)
setExemplars(hist.exemplarsData, timestamp, dpLatency.Exemplars())
dimensions, err := p.getDimensionsByMetricKey(key)
if err != nil {
p.logger.Error(err.Error())
return err
}
dimensions.CopyTo(dpLatency.Attributes())
}
return nil
}
// collectDBCallMetrics collects the raw latency sum and count metrics, writing the data
// into the given instrumentation library metrics.
func (p *processorImp) collectDBCallMetrics(ilm pmetric.ScopeMetrics) error {
mDBCallSum := ilm.Metrics().AppendEmpty()
mDBCallSum.SetName("signoz_db_latency_sum")
mDBCallSum.SetUnit("1")
mDBCallSum.SetEmptySum().SetIsMonotonic(true)
mDBCallSum.Sum().SetAggregationTemporality(p.config.GetAggregationTemporality())
mDBCallCount := ilm.Metrics().AppendEmpty()
mDBCallCount.SetName("signoz_db_latency_count")
mDBCallCount.SetUnit("1")
mDBCallCount.SetEmptySum().SetIsMonotonic(true)
mDBCallCount.Sum().SetAggregationTemporality(p.config.GetAggregationTemporality())
callSumDps := mDBCallSum.Sum().DataPoints()
callCountDps := mDBCallCount.Sum().DataPoints()
callSumDps.EnsureCapacity(len(p.dbCallHistograms))
callCountDps.EnsureCapacity(len(p.dbCallHistograms))
timestamp := pcommon.NewTimestampFromTime(time.Now())
for key, metric := range p.dbCallHistograms {
dpDBCallSum := callSumDps.AppendEmpty()
dpDBCallSum.SetStartTimestamp(p.startTimestamp)
dpDBCallSum.SetTimestamp(timestamp)
dpDBCallSum.SetDoubleValue(metric.sum)
dpDBCallCount := callCountDps.AppendEmpty()
dpDBCallCount.SetStartTimestamp(p.startTimestamp)
dpDBCallCount.SetTimestamp(timestamp)
dpDBCallCount.SetIntValue(int64(metric.count))
dimensions, err := p.getDimensionsByDBCallMetricKey(key)
if err != nil {
p.logger.Error(err.Error())
return err
}
dimensions.CopyTo(dpDBCallSum.Attributes())
dimensions.CopyTo(dpDBCallCount.Attributes())
}
return nil
}
func (p *processorImp) collectExternalCallMetrics(ilm pmetric.ScopeMetrics) error {
mExternalCallSum := ilm.Metrics().AppendEmpty()
mExternalCallSum.SetName("signoz_external_call_latency_sum")
mExternalCallSum.SetUnit("1")
mExternalCallSum.SetEmptySum().SetIsMonotonic(true)
mExternalCallSum.Sum().SetAggregationTemporality(p.config.GetAggregationTemporality())
mExternalCallCount := ilm.Metrics().AppendEmpty()
mExternalCallCount.SetName("signoz_external_call_latency_count")
mExternalCallCount.SetUnit("1")
mExternalCallCount.SetEmptySum().SetIsMonotonic(true)
mExternalCallCount.Sum().SetAggregationTemporality(p.config.GetAggregationTemporality())
callSumDps := mExternalCallSum.Sum().DataPoints()
callCountDps := mExternalCallCount.Sum().DataPoints()
callSumDps.EnsureCapacity(len(p.externalCallHistograms))
callCountDps.EnsureCapacity(len(p.externalCallHistograms))
timestamp := pcommon.NewTimestampFromTime(time.Now())
for key, metric := range p.externalCallHistograms {
dpExternalCallSum := callSumDps.AppendEmpty()
dpExternalCallSum.SetStartTimestamp(p.startTimestamp)
dpExternalCallSum.SetTimestamp(timestamp)
dpExternalCallSum.SetDoubleValue(metric.sum)
dpExternalCallCount := callCountDps.AppendEmpty()
dpExternalCallCount.SetStartTimestamp(p.startTimestamp)
dpExternalCallCount.SetTimestamp(timestamp)
dpExternalCallCount.SetIntValue(int64(metric.count))
dimensions, err := p.getDimensionsByExternalCallMetricKey(key)
if err != nil {
p.logger.Error(err.Error())
return err
}
dimensions.CopyTo(dpExternalCallSum.Attributes())
dimensions.CopyTo(dpExternalCallCount.Attributes())
}
return nil
}
// collectCallMetrics collects the raw call count metrics, writing the data
// into the given instrumentation library metrics.
func (p *processorImp) collectCallMetrics(ilm pmetric.ScopeMetrics) error {
mCalls := ilm.Metrics().AppendEmpty()
mCalls.SetName("signoz_calls_total")
mCalls.SetEmptySum().SetIsMonotonic(true)
mCalls.Sum().SetAggregationTemporality(p.config.GetAggregationTemporality())
dps := mCalls.Sum().DataPoints()
dps.EnsureCapacity(len(p.callHistograms))
timestamp := pcommon.NewTimestampFromTime(time.Now())
for key, hist := range p.callHistograms {
dpCalls := dps.AppendEmpty()
dpCalls.SetStartTimestamp(p.startTimestamp)
dpCalls.SetTimestamp(timestamp)
dpCalls.SetIntValue(int64(hist.count))
dimensions, err := p.getDimensionsByCallMetricKey(key)
if err != nil {
return err
}
dimensions.CopyTo(dpCalls.Attributes())
}
return nil
}
// getDimensionsByMetricKey gets dimensions from `metricKeyToDimensions` cache.
func (p *processorImp) getDimensionsByMetricKey(k metricKey) (pcommon.Map, error) {
if attributeMap, ok := p.metricKeyToDimensions.Get(k); ok {
return attributeMap, nil
}
return pcommon.Map{}, fmt.Errorf("value not found in metricKeyToDimensions cache by key %q", k)
}
// callMetricKeyToDimensions gets dimensions from `callMetricKeyToDimensions` cache.
func (p *processorImp) getDimensionsByCallMetricKey(k metricKey) (pcommon.Map, error) {
if attributeMap, ok := p.callMetricKeyToDimensions.Get(k); ok {
return attributeMap, nil
}
return pcommon.Map{}, fmt.Errorf("value not found in callMetricKeyToDimensions cache by key %q", k)
}
// getDimensionsByDBCallMetricKey gets dimensions from `dbCallMetricKeyToDimensions` cache.
func (p *processorImp) getDimensionsByDBCallMetricKey(k metricKey) (pcommon.Map, error) {
if attributeMap, ok := p.dbCallMetricKeyToDimensions.Get(k); ok {
return attributeMap, nil
}
return pcommon.Map{}, fmt.Errorf("value not found in dbCallMetricKeyToDimensions cache by key %q", k)
}
// getDimensionsByExternalCallMetricKey gets dimensions from `externalCallMetricKeyToDimensions` cache.
func (p *processorImp) getDimensionsByExternalCallMetricKey(k metricKey) (pcommon.Map, error) {
if attributeMap, ok := p.externalCallMetricKeyToDimensions.Get(k); ok {
return attributeMap, nil
}
return pcommon.Map{}, fmt.Errorf("value not found in externalCallMetricKeyToDimensions cache by key %q", k)
}
func getRemoteAddress(span ptrace.Span) (string, bool) {
var addr string
getPeerAddress := func(attrs pcommon.Map) (string, bool) {
var addr string
// Since net.peer.name is readable, it is preferred over net.peer.ip.
peerName, ok := attrs.Get(conventions.AttributeNetPeerName)
if ok {
addr = peerName.Str()
port, ok := attrs.Get(conventions.AttributeNetPeerPort)
if ok {
addr += ":" + port.Str()
}
return addr, true
}
peerIp, ok := attrs.Get(conventions.AttributeNetPeerIP)
if ok {
addr = peerIp.Str()
port, ok := attrs.Get(conventions.AttributeNetPeerPort)
if ok {
addr += ":" + port.Str()
}
return addr, true
}
return "", false
}
attrs := span.Attributes()
_, isRPC := attrs.Get(conventions.AttributeRPCSystem)
// If the span is an RPC, the remote address is service/method.
if isRPC {
service, svcOK := attrs.Get(conventions.AttributeRPCService)
if svcOK {
addr = service.Str()
}
method, methodOK := attrs.Get(conventions.AttributeRPCMethod)
if methodOK {
addr += "/" + method.Str()
}
if addr != "" {
return addr, true
}
// Ideally shouldn't reach here but if for some reason
// service/method not set for RPC, fallback to peer address.
return getPeerAddress(attrs)
}
// If HTTP host is set, use it.
host, ok := attrs.Get(conventions.AttributeHTTPHost)
if ok {
return host.Str(), true
}
peerAddress, ok := getPeerAddress(attrs)
if ok {
// If the peer address is set and the transport is not unix domain socket, or pipe
transport, ok := attrs.Get(conventions.AttributeNetTransport)
if ok && transport.Str() == "unix" && transport.Str() == "pipe" {
return "", false
}
return peerAddress, true
}
// If none of the above is set, check for full URL.
httpURL, ok := attrs.Get(conventions.AttributeHTTPURL)
if ok {
urlValue := httpURL.Str()
// url pattern from godoc [scheme:][//[userinfo@]host][/]path[?query][#fragment]
if !strings.HasPrefix(urlValue, "http://") && !strings.HasPrefix(urlValue, "https://") {
urlValue = "http://" + urlValue
}
parsedURL, err := url.Parse(urlValue)
if err != nil {
return "", false
}
return parsedURL.Host, true
}
peerService, ok := attrs.Get(conventions.AttributePeerService)
if ok {
return peerService.Str(), true
}
return "", false
}
func (p *processorImp) aggregateMetricsForSpan(serviceName string, span ptrace.Span, resourceAttr pcommon.Map) {
if p.shouldSkip(serviceName, span, resourceAttr) {
p.logger.Debug("Skipping span", zap.String("span", span.Name()), zap.String("service", serviceName))
return
}
// Protect against end timestamps before start timestamps. Assume 0 duration.
latencyInMilliseconds := float64(0)
startTime := span.StartTimestamp()
endTime := span.EndTimestamp()
if endTime > startTime {
latencyInMilliseconds = float64(endTime-startTime) / float64(time.Millisecond.Nanoseconds())
}
// Always reset the buffer before re-using.
p.keyBuf.Reset()
buildKey(p.keyBuf, serviceName, span, p.dimensions, resourceAttr)
key := metricKey(p.keyBuf.String())
p.cache(serviceName, span, key, resourceAttr)
p.updateHistogram(key, latencyInMilliseconds, span.TraceID(), span.SpanID())
p.keyBuf.Reset()
buildKey(p.keyBuf, serviceName, span, p.callDimensions, resourceAttr)
callKey := metricKey(p.keyBuf.String())
p.callCache(serviceName, span, callKey, resourceAttr)
p.updateCallHistogram(callKey, latencyInMilliseconds, span.TraceID(), span.SpanID())
spanAttr := span.Attributes()
remoteAddr, externalCallPresent := getRemoteAddress(span)
if span.Kind() == ptrace.SpanKindClient && externalCallPresent {
extraVals := []string{remoteAddr}
p.keyBuf.Reset()
buildCustomKey(p.keyBuf, serviceName, span, p.externalCallDimensions, resourceAttr, extraVals)
externalCallKey := metricKey(p.keyBuf.String())
extraDims := map[string]pcommon.Value{
"address": pcommon.NewValueStr(remoteAddr),
}
p.externalCallCache(serviceName, span, externalCallKey, resourceAttr, extraDims)
p.updateExternalHistogram(externalCallKey, latencyInMilliseconds, span.TraceID(), span.SpanID())
}
_, dbCallPresent := spanAttr.Get("db.system")
if span.Kind() != ptrace.SpanKindServer && dbCallPresent {
p.keyBuf.Reset()
buildCustomKey(p.keyBuf, serviceName, span, p.dbCallDimensions, resourceAttr, nil)
dbCallKey := metricKey(p.keyBuf.String())
p.dbCallCache(serviceName, span, dbCallKey, resourceAttr)
p.updateDBHistogram(dbCallKey, latencyInMilliseconds, span.TraceID(), span.SpanID())
}
}
// aggregateMetrics aggregates the raw metrics from the input trace data.
// Each metric is identified by a key that is built from the service name
// and span metadata such as operation, kind, status_code and any additional
// dimensions the user has configured.
func (p *processorImp) aggregateMetrics(traces ptrace.Traces) {
for i := 0; i < traces.ResourceSpans().Len(); i++ {
rspans := traces.ResourceSpans().At(i)
resourceAttr := rspans.Resource().Attributes()
serviceAttr, ok := resourceAttr.Get(conventions.AttributeServiceName)
if !ok {
continue
}
resourceAttr.PutStr(signozID, p.instanceID)
serviceName := serviceAttr.Str()
ilsSlice := rspans.ScopeSpans()
for j := 0; j < ilsSlice.Len(); j++ {
ils := ilsSlice.At(j)
spans := ils.Spans()
for k := 0; k < spans.Len(); k++ {
span := spans.At(k)
p.aggregateMetricsForSpan(serviceName, span, resourceAttr)
}
}
}
}
// resetAccumulatedMetrics resets the internal maps used to store created metric data. Also purge the cache for
// metricKeyToDimensions.
func (p *processorImp) resetAccumulatedMetrics() {
p.histograms = make(map[metricKey]*histogramData)
p.callHistograms = make(map[metricKey]*histogramData)
p.dbCallHistograms = make(map[metricKey]*histogramData)
p.externalCallHistograms = make(map[metricKey]*histogramData)
p.metricKeyToDimensions.Purge()
p.callMetricKeyToDimensions.Purge()
p.dbCallMetricKeyToDimensions.Purge()
p.externalCallMetricKeyToDimensions.Purge()
}
// updateHistogram adds the histogram sample to the histogram defined by the metric key.
func (p *processorImp) updateHistogram(key metricKey, latency float64, traceID pcommon.TraceID, spanID pcommon.SpanID) {
histo, ok := p.histograms[key]
if !ok {
histo = &histogramData{
bucketCounts: make([]uint64, len(p.latencyBounds)+1),
}
p.histograms[key] = histo
}
histo.sum += latency
histo.count++
// Binary search to find the latencyInMilliseconds bucket index.
index := sort.SearchFloat64s(p.latencyBounds, latency)
histo.bucketCounts[index]++
histo.exemplarsData = append(histo.exemplarsData, exemplarData{traceID: traceID, spanID: spanID, value: latency})
}
func (p *processorImp) updateCallHistogram(key metricKey, latency float64, traceID pcommon.TraceID, spanID pcommon.SpanID) {
histo, ok := p.callHistograms[key]
if !ok {
histo = &histogramData{
bucketCounts: make([]uint64, len(p.latencyBounds)+1),
}
p.callHistograms[key] = histo
}
histo.sum += latency
histo.count++
// Binary search to find the latencyInMilliseconds bucket index.
index := sort.SearchFloat64s(p.latencyBounds, latency)
histo.bucketCounts[index]++
histo.exemplarsData = append(histo.exemplarsData, exemplarData{traceID: traceID, spanID: spanID, value: latency})
}
func (p *processorImp) updateDBHistogram(key metricKey, latency float64, traceID pcommon.TraceID, spanID pcommon.SpanID) {
histo, ok := p.dbCallHistograms[key]
if !ok {
histo = &histogramData{
bucketCounts: make([]uint64, len(p.latencyBounds)+1),
}
p.dbCallHistograms[key] = histo
}
histo.sum += latency
histo.count++
// Binary search to find the latencyInMilliseconds bucket index.
index := sort.SearchFloat64s(p.latencyBounds, latency)
histo.bucketCounts[index]++
histo.exemplarsData = append(histo.exemplarsData, exemplarData{traceID: traceID, spanID: spanID, value: latency})
}
func (p *processorImp) updateExternalHistogram(key metricKey, latency float64, traceID pcommon.TraceID, spanID pcommon.SpanID) {
histo, ok := p.externalCallHistograms[key]
if !ok {
histo = &histogramData{
bucketCounts: make([]uint64, len(p.latencyBounds)+1),
}
p.externalCallHistograms[key] = histo
}
histo.sum += latency
histo.count++
// Binary search to find the latencyInMilliseconds bucket index.
index := sort.SearchFloat64s(p.latencyBounds, latency)
histo.bucketCounts[index]++
histo.exemplarsData = append(histo.exemplarsData, exemplarData{traceID: traceID, spanID: spanID, value: latency})
}
// resetExemplarData resets the entire exemplars map so the next trace will recreate all
// the data structure. An exemplar is a punctual value that exists at specific moment in time
// and should be not considered like a metrics that persist over time.
func (p *processorImp) resetExemplarData() {
for _, histo := range p.histograms {
histo.exemplarsData = nil
}
for _, histo := range p.callHistograms {
histo.exemplarsData = nil
}
for _, histo := range p.dbCallHistograms {
histo.exemplarsData = nil
}
for _, histo := range p.externalCallHistograms {
histo.exemplarsData = nil
}
}
func (p *processorImp) buildDimensionKVs(serviceName string, span ptrace.Span, optionalDims []dimension, resourceAttrs pcommon.Map) pcommon.Map {
dims := pcommon.NewMap()
dims.EnsureCapacity(4 + len(optionalDims))
dims.PutStr(serviceNameKey, serviceName)
dims.PutStr(operationKey, span.Name())
dims.PutStr(spanKindKey, SpanKindStr(span.Kind()))
dims.PutStr(statusCodeKey, StatusCodeStr(span.Status().Code()))
for _, d := range optionalDims {
v, ok, foundInResource := getDimensionValueWithResource(d, span.Attributes(), resourceAttrs)
if ok {
v.CopyTo(dims.PutEmpty(d.name))
}
if foundInResource {
v.CopyTo(dims.PutEmpty(resourcePrefix + d.name))
}
}
dims.Range(func(k string, v pcommon.Value) bool {
if _, exists := p.attrsCardinality[k]; !exists {
p.attrsCardinality[k] = make(map[string]struct{})
}
p.attrsCardinality[k][v.AsString()] = struct{}{}
return true
})
return dims
}
func (p *processorImp) buildCustomDimensionKVs(serviceName string, span ptrace.Span, optionalDims []dimension, resourceAttrs pcommon.Map, extraDims map[string]pcommon.Value) pcommon.Map {
dims := pcommon.NewMap()
dims.PutStr(serviceNameKey, serviceName)
for k, v := range extraDims {
v.CopyTo(dims.PutEmpty(k))
}
dims.PutStr(statusCodeKey, StatusCodeStr(span.Status().Code()))
for _, d := range optionalDims {
v, ok, foundInResource := getDimensionValueWithResource(d, span.Attributes(), resourceAttrs)
if ok {
v.CopyTo(dims.PutEmpty(d.name))
}
if foundInResource {
v.CopyTo(dims.PutEmpty(resourcePrefix + d.name))
}
}
dims.Range(func(k string, v pcommon.Value) bool {
if _, exists := p.attrsCardinality[k]; !exists {
p.attrsCardinality[k] = make(map[string]struct{})
}
p.attrsCardinality[k][v.AsString()] = struct{}{}
return true
})
return dims
}
func concatDimensionValue(dest *bytes.Buffer, value string, prefixSep bool) {
if prefixSep {
dest.WriteString(metricKeySeparator)
}
dest.WriteString(value)
}
// buildKey builds the metric key from the service name and span metadata such as operation, kind, status_code and
// will attempt to add any additional dimensions the user has configured that match the span's attributes
// or resource attributes. If the dimension exists in both, the span's attributes, being the most specific, takes precedence.
//
// The metric key is a simple concatenation of dimension values, delimited by a null character.
func buildKey(dest *bytes.Buffer, serviceName string, span ptrace.Span, optionalDims []dimension, resourceAttrs pcommon.Map) {
concatDimensionValue(dest, serviceName, false)
concatDimensionValue(dest, span.Name(), true)
concatDimensionValue(dest, SpanKindStr(span.Kind()), true)
concatDimensionValue(dest, StatusCodeStr(span.Status().Code()), true)
for _, d := range optionalDims {
if v, ok := getDimensionValue(d, span.Attributes(), resourceAttrs); ok {
concatDimensionValue(dest, v.AsString(), true)
}
}
}
// buildCustomKey builds the metric key from the service name and span metadata such as operation, kind, status_code and
// will attempt to add any additional dimensions the user has configured that match the span's attributes
// or resource attributes. If the dimension exists in both, the span's attributes, being the most specific, takes precedence.
//
// The metric key is a simple concatenation of dimension values, delimited by a null character.
func buildCustomKey(dest *bytes.Buffer, serviceName string, span ptrace.Span, optionalDims []dimension, resourceAttrs pcommon.Map, extraVals []string) {
concatDimensionValue(dest, serviceName, false)
concatDimensionValue(dest, StatusCodeStr(span.Status().Code()), true)
for _, val := range extraVals {
concatDimensionValue(dest, val, true)
}
for _, d := range optionalDims {
v, ok, foundInResource := getDimensionValueWithResource(d, span.Attributes(), resourceAttrs)
if ok {
concatDimensionValue(dest, v.AsString(), true)
}
if foundInResource {
concatDimensionValue(dest, resourcePrefix+v.AsString(), true)
}
}
}
// getDimensionValue gets the dimension value for the given configured dimension.
// It searches through the span's attributes first, being the more specific;
// falling back to searching in resource attributes if it can't be found in the span.
// Finally, falls back to the configured default value if provided.
//
// The ok flag indicates if a dimension value was fetched in order to differentiate
// an empty string value from a state where no value was found.
func getDimensionValue(d dimension, spanAttr pcommon.Map, resourceAttr pcommon.Map) (v pcommon.Value, ok bool) {
// The more specific span attribute should take precedence.
if attr, exists := spanAttr.Get(d.name); exists {
return attr, true
}
if attr, exists := resourceAttr.Get(d.name); exists {
return attr, true
}
// Set the default if configured, otherwise this metric will have no value set for the dimension.
if d.value != nil {
return *d.value, true
}
return v, ok
}
func getDimensionValueWithResource(d dimension, spanAttr pcommon.Map, resourceAttr pcommon.Map) (v pcommon.Value, ok bool, foundInResource bool) {
// The more specific span attribute should take precedence.
if attr, exists := spanAttr.Get(d.name); exists {
return attr, true, false
}
if attr, exists := resourceAttr.Get(d.name); exists {
return attr, true, true
}
// Set the default if configured, otherwise this metric will have no value set for the dimension.
if d.value != nil {
return *d.value, true, false
}
return v, ok, foundInResource
}
// cache the dimension key-value map for the metricKey if there is a cache miss.
// This enables a lookup of the dimension key-value map when constructing the metric like so:
//
// LabelsMap().InitFromMap(p.metricKeyToDimensions[key])
func (p *processorImp) cache(serviceName string, span ptrace.Span, k metricKey, resourceAttrs pcommon.Map) {
// Use Get to ensure any existing key has its recent-ness updated.
if _, has := p.metricKeyToDimensions.Get(k); !has {
p.metricKeyToDimensions.Add(k, p.buildDimensionKVs(serviceName, span, p.dimensions, resourceAttrs))
}
}
// callCache caches the dimension key-value map for the metricKey if there is a cache miss.
// This enables a lookup of the dimension key-value map when constructing the metric like so:
//
// LabelsMap().InitFromMap(p.callMetricKeyToDimensions[key])
func (p *processorImp) callCache(serviceName string, span ptrace.Span, k metricKey, resourceAttrs pcommon.Map) {
// Use Get to ensure any existing key has its recent-ness updated.
if _, has := p.callMetricKeyToDimensions.Get(k); !has {
p.callMetricKeyToDimensions.Add(k, p.buildDimensionKVs(serviceName, span, p.callDimensions, resourceAttrs))
}
}
// dbCallCache caches the dimension key-value map for the metricKey if there is a cache miss.
// This enables a lookup of the dimension key-value map when constructing the metric like so:
//
// LabelsMap().InitFromMap(p.dbCallMetricKeyToDimensions[key])
func (p *processorImp) dbCallCache(serviceName string, span ptrace.Span, k metricKey, resourceAttrs pcommon.Map) {
// Use Get to ensure any existing key has its recent-ness updated.
if _, has := p.dbCallMetricKeyToDimensions.Get(k); !has {
p.dbCallMetricKeyToDimensions.Add(k, p.buildCustomDimensionKVs(serviceName, span, p.dbCallDimensions, resourceAttrs, nil))
}
}
// externalCallCache caches the dimension key-value map for the metricKey if there is a cache miss.
// This enables a lookup of the dimension key-value map when constructing the metric like so:
//
// LabelsMap().InitFromMap(p.externalCallMetricKeyToDimensions[key])
func (p *processorImp) externalCallCache(serviceName string, span ptrace.Span, k metricKey, resourceAttrs pcommon.Map, extraDims map[string]pcommon.Value) {
// Use Get to ensure any existing key has its recent-ness updated.
if _, has := p.externalCallMetricKeyToDimensions.Get(k); !has {
p.externalCallMetricKeyToDimensions.Add(k, p.buildCustomDimensionKVs(serviceName, span, p.externalCallDimensions, resourceAttrs, extraDims))
}
}
// copied from prometheus-go-metric-exporter
// sanitize replaces non-alphanumeric characters with underscores in s.
func sanitize(s string, skipSanitizeLabel bool) string {
if len(s) == 0 {
return s
}
// Note: No length limit for label keys because Prometheus doesn't
// define a length limit, thus we should NOT be truncating label keys.
// See https://github.com/orijtech/prometheus-go-metrics-exporter/issues/4.
s = strings.Map(sanitizeRune, s)
if unicode.IsDigit(rune(s[0])) {
s = "key_" + s
}
// replace labels starting with _ only when skipSanitizeLabel is disabled
if !skipSanitizeLabel && strings.HasPrefix(s, "_") {
s = "key" + s
}
// labels starting with __ are reserved in prometheus
if strings.HasPrefix(s, "__") {
s = "key" + s
}
return s
}
// copied from prometheus-go-metric-exporter
// sanitizeRune converts anything that is not a letter or digit to an underscore
func sanitizeRune(r rune) rune {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
return r
}
// Everything else turns into an underscore
return '_'
}
// setExemplars sets the histogram exemplars.
func setExemplars(exemplarsData []exemplarData, timestamp pcommon.Timestamp, exemplars pmetric.ExemplarSlice) {
es := pmetric.NewExemplarSlice()
es.EnsureCapacity(len(exemplarsData))
for _, ed := range exemplarsData {
value := ed.value
traceID := ed.traceID
spanID := ed.spanID
exemplar := es.AppendEmpty()
if traceID.IsEmpty() {
continue
}
exemplar.SetDoubleValue(value)
exemplar.SetTimestamp(timestamp)
exemplar.SetTraceID(traceID)
exemplar.SetSpanID(spanID)
}
es.CopyTo(exemplars)
}
func SpanKindStr(sk ptrace.SpanKind) string {
switch sk {
case ptrace.SpanKindUnspecified:
return "SPAN_KIND_UNSPECIFIED"
case ptrace.SpanKindInternal:
return "SPAN_KIND_INTERNAL"
case ptrace.SpanKindServer:
return "SPAN_KIND_SERVER"
case ptrace.SpanKindClient:
return "SPAN_KIND_CLIENT"
case ptrace.SpanKindProducer:
return "SPAN_KIND_PRODUCER"
case ptrace.SpanKindConsumer:
return "SPAN_KIND_CONSUMER"
}
return ""
}
func StatusCodeStr(sk ptrace.StatusCode) string {
switch sk {
case ptrace.StatusCodeUnset:
return "STATUS_CODE_UNSET"
case ptrace.StatusCodeOk:
return "STATUS_CODE_OK"
case ptrace.StatusCodeError:
return "STATUS_CODE_ERROR"
}
return ""
}