mirror of
https://github.com/hanzoai/otel-collector.git
synced 2026-10-30 18:26:25 +00:00
Modules declare go 1.27.1 and the build images follow. go fix applied the modernizers; omitzero is not applied, since it changes what the JSON says. Co-authored-by: Hanzo Dev <[email protected]>
1504 lines
53 KiB
Go
1504 lines
53 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 o11yspanmetricsprocessor
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/lightstep/go-expohisto/structure"
|
|
"github.com/tilinna/clock"
|
|
"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"
|
|
"go.opentelemetry.io/collector/pipeline"
|
|
conventions "go.opentelemetry.io/collector/semconv/v1.6.1"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/hanzoai/otel-collector/processor/o11yspanmetricsprocessor/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
|
|
tagHTTPStatusCodeStable = "http.response.status_code"
|
|
metricKeySeparator = string(byte(0))
|
|
traceIDKey = "trace_id"
|
|
|
|
o11yID = "o11y.collector.id"
|
|
|
|
defaultDimensionsCacheSize = 1000
|
|
resourcePrefix = "resource_"
|
|
overflowServiceName = "overflow_service"
|
|
overflowOperation = "overflow_operation"
|
|
defaultTimeBucketInterval = time.Minute
|
|
defaultSkipSpansOlderThan = 24 * time.Hour
|
|
)
|
|
|
|
var (
|
|
defaultLatencyHistogramBucketsMs = []float64{
|
|
2, 4, 6, 8, 10, 50, 100, 200, 400, 800, 1000, 1400, 2000, 5000, 10_000, 15_000,
|
|
}
|
|
)
|
|
|
|
// parseTimesFromKeyOrNow parses the time bucket prefix from a metric key and returns the StartTimeUnixNano and TimeUnixNano fields for metrics.
|
|
// Ref: https://opentelemetry.io/docs/specs/otel/metrics/data-model/#temporality .
|
|
// If the key doesn't have a time bucket prefix (in case of cumulative temporality), it uses the processor start time as StartTimeUnixNano and current time as TimeUnixNano.
|
|
// In case of delta temporality, it uses the start of the bucket as both StartTimeUnixNano & TimeUnixNano.
|
|
func parseTimesFromKeyOrNow(k metricKey, interval time.Duration, now time.Time, processorStartTime pcommon.Timestamp) (start, end pcommon.Timestamp) {
|
|
s := string(k)
|
|
idx := strings.IndexByte(s, 0) // first separator
|
|
if idx <= 0 {
|
|
return processorStartTime, pcommon.NewTimestampFromTime(now)
|
|
}
|
|
u, err := strconv.ParseInt(s[:idx], 10, 64)
|
|
if err != nil {
|
|
// cumulative temporality: StartTimeUnixNano is processor start time, TimeUnixNano is current time
|
|
return processorStartTime, pcommon.NewTimestampFromTime(now)
|
|
}
|
|
bucketStart := time.Unix(u, 0)
|
|
// delta temporality: start of the bucket is both StartTimeUnixNano & TimeUnixNano
|
|
return pcommon.NewTimestampFromTime(bucketStart), pcommon.NewTimestampFromTime(bucketStart)
|
|
}
|
|
|
|
// AddTimeToKeyBuf writes a time-bucket prefix to the key buffer.
|
|
// It truncates the provided time to the configured bucket interval before writing.
|
|
func (p *processorImp) AddTimeToKeyBuf(t time.Time) {
|
|
bucket := t.Truncate(p.config.GetTimeBucketInterval())
|
|
p.keyBuf.WriteString(strconv.FormatInt(bucket.Unix(), 10))
|
|
p.keyBuf.WriteString(metricKeySeparator)
|
|
}
|
|
|
|
// buildMetricKey builds a metric key with optional time bucketing based on temporality.
|
|
// For delta temporality: prepends time bucket prefix
|
|
// For cumulative temporality: no time bucketing (to avoid memory issues)
|
|
func (p *processorImp) buildMetricKey(serviceName string, span ptrace.Span, dimensions []dimension, resourceAttr pcommon.Map) metricKey {
|
|
p.keyBuf.Reset()
|
|
|
|
// Only add time bucket prefix for delta temporality
|
|
if p.config.GetAggregationTemporality() == pmetric.AggregationTemporalityDelta {
|
|
p.AddTimeToKeyBuf(span.StartTimestamp().AsTime())
|
|
}
|
|
|
|
p.buildKey(p.keyBuf, serviceName, span, dimensions, resourceAttr)
|
|
return metricKey(p.keyBuf.String())
|
|
}
|
|
|
|
// buildCustomMetricKey builds a custom metric key with optional time bucketing based on temporality.
|
|
// For delta temporality: prepends time bucket prefix
|
|
// For cumulative temporality: no time bucketing (to avoid memory issues)
|
|
func (p *processorImp) buildCustomMetricKey(serviceName string, span ptrace.Span, dimensions []dimension, resourceAttr pcommon.Map, extraVals []string) metricKey {
|
|
p.keyBuf.Reset()
|
|
|
|
// Only add time bucket prefix for delta temporality
|
|
if p.config.GetAggregationTemporality() == pmetric.AggregationTemporalityDelta {
|
|
p.AddTimeToKeyBuf(span.StartTimestamp().AsTime())
|
|
}
|
|
|
|
buildCustomKey(p.keyBuf, serviceName, span, dimensions, resourceAttr, extraVals)
|
|
return metricKey(p.keyBuf.String())
|
|
}
|
|
|
|
type exemplarData struct {
|
|
traceID pcommon.TraceID
|
|
spanID pcommon.SpanID
|
|
value float64
|
|
}
|
|
|
|
type exponentialHistogram struct {
|
|
histogram *structure.Histogram[float64]
|
|
}
|
|
|
|
type metricKey string
|
|
|
|
type processorImp struct {
|
|
lock sync.Mutex
|
|
logger *zap.Logger
|
|
instanceID string
|
|
config Config
|
|
|
|
metricsConsumer []consumer.Metrics
|
|
tracesConsumer consumer.Traces
|
|
|
|
// Additional dimensions to add to metrics.
|
|
dimensions []dimension // o11y_latency metric
|
|
expDimensions []dimension // o11y_latency exphisto metric
|
|
callDimensions []dimension // o11y_calls_total metric
|
|
dbCallDimensions []dimension // o11y_db_latency_* metric
|
|
externalCallDimensions []dimension // o11y_external_call_latency_* metric
|
|
|
|
// The starting time of the data points.
|
|
startTimestamp pcommon.Timestamp
|
|
|
|
// Histogram.
|
|
histograms map[metricKey]*histogramData // o11y_latency metric
|
|
latencyBounds []float64
|
|
|
|
expHistograms map[metricKey]*exponentialHistogram
|
|
|
|
callHistograms map[metricKey]*histogramData // o11y_calls_total metric
|
|
callLatencyBounds []float64
|
|
|
|
dbCallHistograms map[metricKey]*histogramData // o11y_db_latency_* metric
|
|
dbCallLatencyBounds []float64
|
|
|
|
externalCallHistograms map[metricKey]*histogramData // o11y_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]
|
|
expHistogramKeyToDimensions *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
|
|
|
|
ticker *clock.Ticker
|
|
done chan struct{}
|
|
started bool
|
|
wg sync.WaitGroup
|
|
|
|
shutdownOnce sync.Once
|
|
|
|
serviceToOperations map[string]map[string]struct{}
|
|
maxNumberOfServicesToTrack int
|
|
maxNumberOfOperationsToTrackPerService int
|
|
}
|
|
|
|
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 expoHistToExponentialDataPoint(agg *structure.Histogram[float64], dp pmetric.ExponentialHistogramDataPoint) {
|
|
dp.SetCount(agg.Count())
|
|
dp.SetSum(agg.Sum())
|
|
if agg.Count() != 0 {
|
|
dp.SetMin(agg.Min())
|
|
dp.SetMax(agg.Max())
|
|
}
|
|
|
|
dp.SetZeroCount(agg.ZeroCount())
|
|
dp.SetScale(agg.Scale())
|
|
|
|
for _, half := range []struct {
|
|
inFunc func() *structure.Buckets
|
|
outFunc func() pmetric.ExponentialHistogramDataPointBuckets
|
|
}{
|
|
{agg.Positive, dp.Positive},
|
|
{agg.Negative, dp.Negative},
|
|
} {
|
|
in := half.inFunc()
|
|
out := half.outFunc()
|
|
out.SetOffset(in.Offset())
|
|
out.BucketCounts().EnsureCapacity(int(in.Len()))
|
|
|
|
for i := uint32(0); i < in.Len(); i++ {
|
|
out.BucketCounts().Append(in.At(i))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *exponentialHistogram) Observe(value float64) {
|
|
h.histogram.Update(value)
|
|
}
|
|
|
|
func newProcessor(logger *zap.Logger, instanceID string, config component.Config, ticker *clock.Ticker) (*processorImp, error) {
|
|
logger.Info("Building o11yspanmetricsprocessor with config", zap.Any("config", config))
|
|
pConfig := config.(*Config)
|
|
|
|
// Set default time bucket interval if not specified
|
|
if pConfig.TimeBucketInterval == 0 {
|
|
pConfig.TimeBucketInterval = defaultTimeBucketInterval
|
|
}
|
|
|
|
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
|
|
}
|
|
expHistogramKeyToDimensionsCache, 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),
|
|
expHistograms: make(map[metricKey]*exponentialHistogram),
|
|
callHistograms: make(map[metricKey]*histogramData),
|
|
dbCallHistograms: make(map[metricKey]*histogramData),
|
|
externalCallHistograms: make(map[metricKey]*histogramData),
|
|
latencyBounds: bounds,
|
|
callLatencyBounds: bounds,
|
|
dbCallLatencyBounds: bounds,
|
|
externalCallLatencyBounds: bounds,
|
|
dimensions: newDimensions(pConfig.Dimensions),
|
|
expDimensions: newDimensions(pConfig.Dimensions),
|
|
callDimensions: newDimensions(callDimensions),
|
|
dbCallDimensions: newDimensions(dbCallDimensions),
|
|
externalCallDimensions: newDimensions(externalCallDimensions),
|
|
keyBuf: bytes.NewBuffer(make([]byte, 0, 1024)),
|
|
metricKeyToDimensions: metricKeyToDimensionsCache,
|
|
expHistogramKeyToDimensions: expHistogramKeyToDimensionsCache,
|
|
callMetricKeyToDimensions: callMetricKeyToDimensionsCache,
|
|
dbCallMetricKeyToDimensions: dbMetricKeyToDimensionsCache,
|
|
externalCallMetricKeyToDimensions: externalCallMetricKeyToDimensionsCache,
|
|
attrsCardinality: make(map[string]map[string]struct{}),
|
|
excludePatternRegex: excludePatternRegex,
|
|
ticker: ticker,
|
|
done: make(chan struct{}),
|
|
serviceToOperations: make(map[string]map[string]struct{}),
|
|
maxNumberOfServicesToTrack: pConfig.MaxServicesToTrack,
|
|
maxNumberOfOperationsToTrackPerService: pConfig.MaxOperationsToTrackPerService,
|
|
}, 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
|
|
}
|
|
|
|
type getExporters interface {
|
|
GetExporters() map[pipeline.Signal]map[component.ID]component.Component
|
|
}
|
|
|
|
// Start implements the component.Component interface.
|
|
func (p *processorImp) Start(ctx context.Context, host component.Host) error {
|
|
p.logger.Info("Starting o11yspanmetricsprocessor with config", zap.Any("config", p.config))
|
|
|
|
ge, ok := host.(getExporters)
|
|
if !ok {
|
|
return fmt.Errorf("unable to get exporters")
|
|
}
|
|
|
|
exporters := ge.GetExporters()
|
|
|
|
availableMetricsExporters := make([]string, 0, len(exporters[pipeline.SignalMetrics]))
|
|
exporterNames := strings.Split(p.config.MetricsExporter, ",")
|
|
for i := range exporterNames {
|
|
exporterNames[i] = strings.TrimSpace(exporterNames[i])
|
|
}
|
|
// The available list of exporters come from any configured metrics pipelines' exporters.
|
|
for k, exp := range exporters[pipeline.SignalMetrics] {
|
|
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 o11yspanmetrics exporter from available exporters",
|
|
zap.String("o11yspanmetrics-exporter", p.config.MetricsExporter),
|
|
zap.Any("available-exporters", availableMetricsExporters),
|
|
)
|
|
for _, name := range exporterNames {
|
|
if k.String() == name {
|
|
p.metricsConsumer = append(p.metricsConsumer, metricsExp)
|
|
p.logger.Info("Found exporter", zap.String("o11yspanmetrics-exporter", name))
|
|
}
|
|
}
|
|
}
|
|
if len(p.metricsConsumer) == 0 {
|
|
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 o11yspanmetricsprocessor")
|
|
|
|
p.started = true
|
|
p.wg.Go(func() {
|
|
for {
|
|
select {
|
|
case <-p.done:
|
|
return
|
|
case <-p.ticker.C:
|
|
p.exportMetrics(ctx)
|
|
}
|
|
}
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Shutdown implements the component.Component interface.
|
|
func (p *processorImp) Shutdown(ctx context.Context) error {
|
|
p.logger.Info("Shutting down o11yspanmetricsprocessor")
|
|
p.shutdownOnce.Do(func() {
|
|
if p.started {
|
|
p.logger.Info("Stopping ticker")
|
|
p.ticker.Stop()
|
|
close(p.done)
|
|
p.started = false
|
|
}
|
|
})
|
|
p.wg.Wait()
|
|
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 {
|
|
p.lock.Lock()
|
|
p.aggregateMetrics(traces)
|
|
p.lock.Unlock()
|
|
|
|
// Forward trace data unmodified and propagate trace pipeline errors, if any.
|
|
return p.tracesConsumer.ConsumeTraces(ctx, traces)
|
|
}
|
|
|
|
func (p *processorImp) exportMetrics(ctx context.Context) {
|
|
p.lock.Lock()
|
|
|
|
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()
|
|
p.logger.Error("Failed to build metrics", zap.Error(err))
|
|
}
|
|
exporterNames := strings.Split(p.config.MetricsExporter, ",")
|
|
for i, consumer := range p.metricsConsumer {
|
|
if err := consumer.ConsumeMetrics(ctx, m); err != nil {
|
|
p.logger.Error("Failed ConsumeMetrics for exporter", zap.String("exporter", exporterNames[i]), zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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("o11yspanmetricsprocessor")
|
|
|
|
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
|
|
}
|
|
|
|
if err := p.collectExpHistogramMetrics(ilm); err != nil {
|
|
return pmetric.Metrics{}, err
|
|
}
|
|
|
|
p.metricKeyToDimensions.RemoveEvictedItems()
|
|
p.expHistogramKeyToDimensions.RemoveEvictedItems()
|
|
p.callMetricKeyToDimensions.RemoveEvictedItems()
|
|
p.dbCallMetricKeyToDimensions.RemoveEvictedItems()
|
|
p.externalCallMetricKeyToDimensions.RemoveEvictedItems()
|
|
|
|
for key := range p.histograms {
|
|
if !p.metricKeyToDimensions.Contains(key) {
|
|
delete(p.histograms, key)
|
|
}
|
|
}
|
|
for key := range p.callHistograms {
|
|
if !p.callMetricKeyToDimensions.Contains(key) {
|
|
delete(p.callHistograms, key)
|
|
}
|
|
}
|
|
for key := range p.dbCallHistograms {
|
|
if !p.dbCallMetricKeyToDimensions.Contains(key) {
|
|
delete(p.dbCallHistograms, key)
|
|
}
|
|
}
|
|
for key := range p.externalCallHistograms {
|
|
if !p.externalCallMetricKeyToDimensions.Contains(key) {
|
|
delete(p.externalCallHistograms, key)
|
|
}
|
|
}
|
|
|
|
for key := range p.expHistograms {
|
|
if !p.expHistogramKeyToDimensions.Contains(key) {
|
|
delete(p.expHistograms, key)
|
|
}
|
|
}
|
|
|
|
// 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("o11y_latency")
|
|
mLatency.SetUnit("ms")
|
|
mLatency.SetEmptyHistogram().SetAggregationTemporality(p.config.GetAggregationTemporality())
|
|
dps := mLatency.Histogram().DataPoints()
|
|
dps.EnsureCapacity(len(p.histograms))
|
|
|
|
for key, hist := range p.histograms {
|
|
dpLatency := dps.AppendEmpty()
|
|
start, end := parseTimesFromKeyOrNow(key, p.config.GetTimeBucketInterval(), time.Now(), p.startTimestamp)
|
|
dpLatency.SetStartTimestamp(start)
|
|
dpLatency.SetTimestamp(end)
|
|
dpLatency.ExplicitBounds().FromRaw(p.latencyBounds)
|
|
dpLatency.BucketCounts().FromRaw(hist.bucketCounts)
|
|
dpLatency.SetCount(hist.count)
|
|
dpLatency.SetSum(hist.sum)
|
|
setExemplars(hist.exemplarsData, end, dpLatency.Exemplars())
|
|
|
|
dimensions, err := p.getDimensionsByMetricKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dimensions.CopyTo(dpLatency.Attributes())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// collectExpHistogramMetrics collects the raw latency metrics, writing the data
|
|
// into the given instrumentation library metrics.
|
|
func (p *processorImp) collectExpHistogramMetrics(ilm pmetric.ScopeMetrics) error {
|
|
mExpLatency := ilm.Metrics().AppendEmpty()
|
|
mExpLatency.SetName("o11y_latency")
|
|
mExpLatency.SetUnit("ms")
|
|
mExpLatency.SetEmptyExponentialHistogram().SetAggregationTemporality(p.config.GetAggregationTemporality())
|
|
dps := mExpLatency.ExponentialHistogram().DataPoints()
|
|
dps.EnsureCapacity(len(p.expHistograms))
|
|
|
|
for key, hist := range p.expHistograms {
|
|
dp := dps.AppendEmpty()
|
|
start, end := parseTimesFromKeyOrNow(key, p.config.GetTimeBucketInterval(), time.Now(), p.startTimestamp)
|
|
dp.SetStartTimestamp(start)
|
|
dp.SetTimestamp(end)
|
|
expoHistToExponentialDataPoint(hist.histogram, dp)
|
|
dimensions, err := p.getDimensionsByExpHistogramKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dimensions.CopyTo(dps.At(dps.Len() - 1).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("o11y_db_latency_sum")
|
|
mDBCallSum.SetUnit("1")
|
|
mDBCallSum.SetEmptySum().SetIsMonotonic(true)
|
|
mDBCallSum.Sum().SetAggregationTemporality(p.config.GetAggregationTemporality())
|
|
|
|
mDBCallCount := ilm.Metrics().AppendEmpty()
|
|
mDBCallCount.SetName("o11y_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))
|
|
|
|
for key, metric := range p.dbCallHistograms {
|
|
dpDBCallSum := callSumDps.AppendEmpty()
|
|
start, end := parseTimesFromKeyOrNow(key, p.config.GetTimeBucketInterval(), time.Now(), p.startTimestamp)
|
|
dpDBCallSum.SetStartTimestamp(start)
|
|
dpDBCallSum.SetTimestamp(end)
|
|
dpDBCallSum.SetDoubleValue(metric.sum)
|
|
|
|
dpDBCallCount := callCountDps.AppendEmpty()
|
|
dpDBCallCount.SetStartTimestamp(start)
|
|
dpDBCallCount.SetTimestamp(end)
|
|
dpDBCallCount.SetIntValue(int64(metric.count))
|
|
|
|
dimensions, err := p.getDimensionsByDBCallMetricKey(key)
|
|
if err != nil {
|
|
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("o11y_external_call_latency_sum")
|
|
mExternalCallSum.SetUnit("1")
|
|
mExternalCallSum.SetEmptySum().SetIsMonotonic(true)
|
|
mExternalCallSum.Sum().SetAggregationTemporality(p.config.GetAggregationTemporality())
|
|
|
|
mExternalCallCount := ilm.Metrics().AppendEmpty()
|
|
mExternalCallCount.SetName("o11y_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))
|
|
|
|
for key, metric := range p.externalCallHistograms {
|
|
dpExternalCallSum := callSumDps.AppendEmpty()
|
|
start, end := parseTimesFromKeyOrNow(key, p.config.GetTimeBucketInterval(), time.Now(), p.startTimestamp)
|
|
dpExternalCallSum.SetStartTimestamp(start)
|
|
dpExternalCallSum.SetTimestamp(end)
|
|
dpExternalCallSum.SetDoubleValue(metric.sum)
|
|
|
|
dpExternalCallCount := callCountDps.AppendEmpty()
|
|
dpExternalCallCount.SetStartTimestamp(start)
|
|
dpExternalCallCount.SetTimestamp(end)
|
|
dpExternalCallCount.SetIntValue(int64(metric.count))
|
|
|
|
dimensions, err := p.getDimensionsByExternalCallMetricKey(key)
|
|
if err != nil {
|
|
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("o11y_calls_total")
|
|
mCalls.SetEmptySum().SetIsMonotonic(true)
|
|
mCalls.Sum().SetAggregationTemporality(p.config.GetAggregationTemporality())
|
|
dps := mCalls.Sum().DataPoints()
|
|
dps.EnsureCapacity(len(p.callHistograms))
|
|
|
|
for key, hist := range p.callHistograms {
|
|
dpCalls := dps.AppendEmpty()
|
|
start, end := parseTimesFromKeyOrNow(key, p.config.GetTimeBucketInterval(), time.Now(), p.startTimestamp)
|
|
dpCalls.SetStartTimestamp(start)
|
|
dpCalls.SetTimestamp(end)
|
|
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)
|
|
}
|
|
|
|
// getDimensionsByExpHistogramKey gets dimensions from `expHistogramKeyToDimensions` cache.
|
|
func (p *processorImp) getDimensionsByExpHistogramKey(k metricKey) (pcommon.Map, error) {
|
|
if attributeMap, ok := p.expHistogramKeyToDimensions.Get(k); ok {
|
|
return attributeMap, nil
|
|
}
|
|
return pcommon.Map{}, fmt.Errorf("value not found in expHistogramKeyToDimensions 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
|
|
}
|
|
// net.peer.name|net.host.name is renamed to server.address
|
|
peerAddress, ok := attrs.Get("server.address")
|
|
if ok {
|
|
addr = peerAddress.Str()
|
|
port, ok := attrs.Get("server.port")
|
|
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
|
|
}
|
|
// net.peer.ip is renamed to net.sock.peer.addr
|
|
peerAddress, ok = attrs.Get("net.sock.peer.addr")
|
|
if ok {
|
|
addr = peerAddress.Str()
|
|
port, ok := attrs.Get("net.sock.peer.port")
|
|
if ok {
|
|
addr += ":" + port.Str()
|
|
}
|
|
return addr, true
|
|
}
|
|
|
|
// And later net.sock.peer.addr is renamed to network.peer.address
|
|
peerAddress, ok = attrs.Get("network.peer.address")
|
|
if ok {
|
|
addr = peerAddress.Str()
|
|
port, ok := attrs.Get("network.peer.port")
|
|
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 {
|
|
// http.url is renamed to url.full
|
|
httpURL, ok = attrs.Get("url.full")
|
|
}
|
|
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) {
|
|
// Skip late-arriving spans older than the configured staleness spanSkipWindow
|
|
if spanSkipWindow := p.config.GetSkipSpansOlderThan(); spanSkipWindow > 0 {
|
|
lastPermissibleSpanTime := time.Now().Add(-spanSkipWindow)
|
|
if span.StartTimestamp().AsTime().Before(lastPermissibleSpanTime) {
|
|
p.logger.Debug("Skipping stale span", zap.String("span", span.Name()), zap.String("service", serviceName))
|
|
return
|
|
}
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
// Build key for latency metrics (with conditional time bucketing)
|
|
key := p.buildMetricKey(serviceName, span, p.dimensions, resourceAttr)
|
|
p.cache(serviceName, span, key, resourceAttr)
|
|
p.updateHistogram(key, latencyInMilliseconds, span.TraceID(), span.SpanID())
|
|
|
|
if p.config.EnableExpHistogram {
|
|
expKey := p.buildMetricKey(serviceName, span, p.expDimensions, resourceAttr)
|
|
p.expHistogramCache(serviceName, span, expKey, resourceAttr)
|
|
p.updateExpHistogram(expKey, latencyInMilliseconds, span.TraceID(), span.SpanID())
|
|
}
|
|
|
|
// Build key for call metrics (with conditional time bucketing)
|
|
callKey := p.buildMetricKey(serviceName, span, p.callDimensions, resourceAttr)
|
|
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}
|
|
externalCallKey := p.buildCustomMetricKey(serviceName, span, p.externalCallDimensions, resourceAttr, extraVals)
|
|
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 {
|
|
dbCallKey := p.buildCustomMetricKey(serviceName, span, p.dbCallDimensions, resourceAttr, nil)
|
|
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(o11yID, 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.expHistograms = make(map[metricKey]*exponentialHistogram)
|
|
p.callHistograms = make(map[metricKey]*histogramData)
|
|
p.dbCallHistograms = make(map[metricKey]*histogramData)
|
|
p.externalCallHistograms = make(map[metricKey]*histogramData)
|
|
|
|
p.metricKeyToDimensions.Purge()
|
|
p.expHistogramKeyToDimensions.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) updateExpHistogram(key metricKey, latency float64, traceID pcommon.TraceID, spanID pcommon.SpanID) {
|
|
histo, ok := p.expHistograms[key]
|
|
if !ok {
|
|
histogram := new(structure.Histogram[float64])
|
|
cfg := structure.NewConfig(
|
|
structure.WithMaxSize(structure.DefaultMaxSize),
|
|
)
|
|
histogram.Init(cfg)
|
|
|
|
histo = &exponentialHistogram{
|
|
histogram: histogram,
|
|
}
|
|
p.expHistograms[key] = histo
|
|
}
|
|
|
|
histo.Observe(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 {
|
|
|
|
spanName := span.Name()
|
|
if len(p.serviceToOperations) > p.maxNumberOfServicesToTrack {
|
|
serviceName = overflowServiceName
|
|
}
|
|
if len(p.serviceToOperations[serviceName]) > p.maxNumberOfOperationsToTrackPerService {
|
|
spanName = overflowOperation
|
|
}
|
|
|
|
dims := pcommon.NewMap()
|
|
dims.EnsureCapacity(4 + len(optionalDims))
|
|
dims.PutStr(serviceNameKey, serviceName)
|
|
dims.PutStr(operationKey, spanName)
|
|
dims.PutStr(spanKindKey, SpanKindStr(span.Kind()))
|
|
dims.PutStr(statusCodeKey, StatusCodeStr(span.Status().Code()))
|
|
dims.PutStr(o11yID, p.instanceID)
|
|
dims.PutStr(resourcePrefix+o11yID, p.instanceID)
|
|
for _, d := range optionalDims {
|
|
// Already tagged above from the collector instance ID.
|
|
if d.name == o11yID {
|
|
continue
|
|
}
|
|
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()))
|
|
dims.PutStr(o11yID, p.instanceID)
|
|
dims.PutStr(resourcePrefix+o11yID, p.instanceID)
|
|
for _, d := range optionalDims {
|
|
// Already tagged above from the collector instance ID.
|
|
if d.name == o11yID {
|
|
continue
|
|
}
|
|
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 (p *processorImp) buildKey(dest *bytes.Buffer, serviceName string, span ptrace.Span, optionalDims []dimension, resourceAttrs pcommon.Map) {
|
|
spanName := span.Name()
|
|
if len(p.serviceToOperations) > p.maxNumberOfServicesToTrack {
|
|
p.logger.Warn("Too many services to track, using overflow service name",
|
|
zap.Int("maxNumberOfServicesToTrack", p.maxNumberOfServicesToTrack))
|
|
serviceName = overflowServiceName
|
|
}
|
|
if len(p.serviceToOperations[serviceName]) > p.maxNumberOfOperationsToTrackPerService {
|
|
p.logger.Warn("Too many operations to track, using overflow operation name",
|
|
zap.Int("maxNumberOfOperationsToTrackPerService", p.maxNumberOfOperationsToTrackPerService),
|
|
zap.String("serviceName", serviceName))
|
|
spanName = overflowOperation
|
|
}
|
|
concatDimensionValue(dest, serviceName, false)
|
|
concatDimensionValue(dest, spanName, 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)
|
|
}
|
|
}
|
|
if _, ok := p.serviceToOperations[serviceName]; !ok {
|
|
p.serviceToOperations[serviceName] = make(map[string]struct{})
|
|
}
|
|
p.serviceToOperations[serviceName][spanName] = struct{}{}
|
|
|
|
}
|
|
|
|
// 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
|
|
} else if d.name == tagHTTPStatusCode {
|
|
if attr, exists := spanAttr.Get(tagHTTPStatusCodeStable); 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) {
|
|
if attr, exists := spanAttr.Get(d.name); exists {
|
|
if _, exists := resourceAttr.Get(d.name); exists {
|
|
return attr, true, true
|
|
}
|
|
return attr, true, false
|
|
} else if d.name == tagHTTPStatusCode {
|
|
if attr, exists := spanAttr.Get(tagHTTPStatusCodeStable); 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))
|
|
}
|
|
}
|
|
|
|
// expHistogramCache 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.expHistogramKeyToDimensions[key])
|
|
func (p *processorImp) expHistogramCache(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.expHistogramKeyToDimensions.Get(k); !has {
|
|
p.expHistogramKeyToDimensions.Add(k, p.buildDimensionKVs(serviceName, span, p.expDimensions, 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 ""
|
|
}
|