-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: add self-observability metrics to otlpmetricgrpc metric exporters #7120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7938b6d
8c6576b
208e490
4271508
7508037
89273ec
5bfa165
dfeedaf
9cc2b4e
0a1f30c
0b13731
a280dd3
b95f9e9
b01825b
894c26c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| // Package selfobservability provides self-observability metrics for OTLP metric exporters. | ||
| // This is an experimental feature controlled by the x.SelfObservability feature flag. | ||
| package selfobservability // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/selfobservability" | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "net/url" | ||
| "strconv" | ||
| "strings" | ||
| "sync/atomic" | ||
| "time" | ||
|
|
||
| "go.opentelemetry.io/otel" | ||
| "go.opentelemetry.io/otel/attribute" | ||
| "go.opentelemetry.io/otel/metric" | ||
| "go.opentelemetry.io/otel/sdk" | ||
| "go.opentelemetry.io/otel/sdk/metric/metricdata" | ||
| semconv "go.opentelemetry.io/otel/semconv/v1.36.0" | ||
| "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" | ||
|
|
||
| "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/x" | ||
| ) | ||
|
|
||
| // exporterIDCounter is used to generate unique component names for exporters. | ||
| var exporterIDCounter atomic.Uint64 | ||
|
|
||
| // nextExporterID returns the next unique exporter ID. | ||
| func nextExporterID() uint64 { | ||
| return exporterIDCounter.Add(1) - 1 | ||
| } | ||
|
|
||
| // ExporterMetrics holds the self-observability metric instruments for an OTLP metric exporter. | ||
| type ExporterMetrics struct { | ||
| exported otelconv.SDKExporterMetricDataPointExported | ||
| inflight otelconv.SDKExporterMetricDataPointInflight | ||
| duration otelconv.SDKExporterOperationDuration | ||
| attrs []attribute.KeyValue | ||
| enabled bool | ||
| } | ||
|
|
||
| // NewExporterMetrics creates a new ExporterMetrics instance. | ||
| // If self-observability is disabled, returns a no-op instance. | ||
| func NewExporterMetrics(componentType, serverAddress string, serverPort int) *ExporterMetrics { | ||
| em := &ExporterMetrics{ | ||
| enabled: x.SelfObservability.Enabled(), | ||
| } | ||
|
|
||
| if !em.enabled { | ||
| return em | ||
| } | ||
|
|
||
| meter := otel.GetMeterProvider().Meter( | ||
| "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc", | ||
| metric.WithInstrumentationVersion(sdk.Version()), | ||
| metric.WithSchemaURL(semconv.SchemaURL), | ||
| ) | ||
|
|
||
| var err error | ||
| em.exported, err = otelconv.NewSDKExporterMetricDataPointExported(meter) | ||
| if err != nil { | ||
| em.enabled = false | ||
| return em | ||
| } | ||
|
|
||
| em.inflight, err = otelconv.NewSDKExporterMetricDataPointInflight(meter) | ||
| if err != nil { | ||
| em.enabled = false | ||
| return em | ||
| } | ||
|
|
||
| em.duration, err = otelconv.NewSDKExporterOperationDuration(meter) | ||
| if err != nil { | ||
| em.enabled = false | ||
| return em | ||
| } | ||
|
|
||
| // Set up common attributes | ||
| componentName := fmt.Sprintf("%s/%d", componentType, nextExporterID()) | ||
| em.attrs = []attribute.KeyValue{ | ||
|
minimAluminiumalism marked this conversation as resolved.
|
||
| semconv.OTelComponentTypeKey.String(componentType), | ||
| semconv.OTelComponentName(componentName), | ||
| semconv.ServerAddress(serverAddress), | ||
| semconv.ServerPort(serverPort), | ||
| } | ||
|
|
||
| return em | ||
| } | ||
|
|
||
| // TrackExport tracks an export operation and returns a function to complete the tracking. | ||
| // The returned function should be called when the export operation completes. | ||
| func (em *ExporterMetrics) TrackExport(ctx context.Context, rm *metricdata.ResourceMetrics) func(error) { | ||
| if !em.enabled { | ||
| return func(error) {} | ||
| } | ||
|
|
||
| dataPointCount := countDataPoints(rm) | ||
| startTime := time.Now() | ||
|
|
||
| em.inflight.Add(ctx, dataPointCount, em.attrs...) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This and calls below should be pooling options. See https://github.com/open-telemetry/opentelemetry-go/blob/main/CONTRIBUTING.md#attribute-and-option-allocation-management
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should check if each instrument is Enabled(ctx) prior to making Add or Record calls. |
||
|
|
||
| return func(err error) { | ||
| em.inflight.Add(ctx, -dataPointCount, em.attrs...) | ||
|
|
||
| duration := time.Since(startTime).Seconds() | ||
|
|
||
| attrs := make([]attribute.KeyValue, len(em.attrs), len(em.attrs)+1) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should pool attribute slices here. |
||
| copy(attrs, em.attrs) | ||
| if err != nil { | ||
| attrs = append(attrs, semconv.ErrorTypeOther) | ||
| } | ||
| em.duration.Inst().Record(ctx, duration, metric.WithAttributes(attrs...)) | ||
|
|
||
| if err == nil { | ||
| em.exported.Add(ctx, dataPointCount, em.attrs...) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // countDataPoints counts the total number of data points in a ResourceMetrics. | ||
| func countDataPoints(rm *metricdata.ResourceMetrics) int64 { | ||
| if rm == nil { | ||
| return 0 | ||
| } | ||
|
|
||
| var total int64 | ||
| for _, sm := range rm.ScopeMetrics { | ||
| for _, m := range sm.Metrics { | ||
| switch data := m.Data.(type) { | ||
| case metricdata.Gauge[int64]: | ||
| total += int64(len(data.DataPoints)) | ||
| case metricdata.Gauge[float64]: | ||
| total += int64(len(data.DataPoints)) | ||
| case metricdata.Sum[int64]: | ||
| total += int64(len(data.DataPoints)) | ||
| case metricdata.Sum[float64]: | ||
| total += int64(len(data.DataPoints)) | ||
| case metricdata.Histogram[int64]: | ||
| total += int64(len(data.DataPoints)) | ||
| case metricdata.Histogram[float64]: | ||
| total += int64(len(data.DataPoints)) | ||
| case metricdata.ExponentialHistogram[int64]: | ||
| total += int64(len(data.DataPoints)) | ||
| case metricdata.ExponentialHistogram[float64]: | ||
| total += int64(len(data.DataPoints)) | ||
| case metricdata.Summary: | ||
| total += int64(len(data.DataPoints)) | ||
| } | ||
| } | ||
| } | ||
| return total | ||
| } | ||
|
|
||
| // ParseEndpoint extracts server address and port from an endpoint URL. | ||
| // Returns defaults if parsing fails or endpoint is empty. | ||
| func ParseEndpoint(endpoint string) (address string, port int) { | ||
| address = "localhost" | ||
| port = 4317 | ||
|
|
||
| if endpoint == "" { | ||
| return | ||
| } | ||
|
|
||
| // Handle endpoint without scheme | ||
| if !strings.Contains(endpoint, "://") { | ||
| endpoint = "http://" + endpoint | ||
| } | ||
|
|
||
| u, err := url.Parse(endpoint) | ||
| if err != nil { | ||
| return | ||
| } | ||
|
|
||
| if u.Hostname() != "" { | ||
| address = u.Hostname() | ||
| } | ||
|
|
||
| if u.Port() != "" { | ||
| if p, err := strconv.Atoi(u.Port()); err == nil { | ||
| port = p | ||
| } | ||
| } | ||
|
|
||
| return | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update to semconv 1.40