e0e362d700
SDK Tests / SDK CI (push) Blocked by required conditions
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
SDK Tests / Python SDK Tests (sandbox) (push) Waiting to run
SDK Tests / CLI Quality (push) Waiting to run
SDK Tests / CLI Tests (push) Waiting to run
SDK Tests / Python SDK Quality (code-interpreter) (push) Waiting to run
SDK Tests / Python SDK Quality (sandbox) (push) Waiting to run
SDK Tests / Python SDK Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Go SDK Quality And Tests (push) Waiting to run
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
186 lines
5.1 KiB
Go
186 lines
5.1 KiB
Go
// Copyright 2026 Alibaba Group Holding Ltd.
|
|
//
|
|
// 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 telemetry provides shared OpenTelemetry OTLP metrics setup for OpenSandbox binaries.
|
|
package telemetry
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
|
|
"go.opentelemetry.io/otel"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
|
|
"go.opentelemetry.io/otel/metric/noop"
|
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
|
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
|
"go.opentelemetry.io/otel/sdk/resource"
|
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
|
tracenoop "go.opentelemetry.io/otel/trace/noop"
|
|
)
|
|
|
|
// Config controls OTLP metrics export. Endpoints follow standard OTEL env vars; see metricsEnabled.
|
|
type Config struct {
|
|
ServiceName string
|
|
ResourceAttributes []attribute.KeyValue
|
|
RegisterMetrics func() error
|
|
}
|
|
|
|
const (
|
|
envOTLPMetricsEndpoint = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"
|
|
envOTLPEndpoint = "OTEL_EXPORTER_OTLP_ENDPOINT"
|
|
envHostIP = "HOST_IP"
|
|
otlpHTTPPort = "4318"
|
|
hostInfoPath = "/etc/hostinfo"
|
|
)
|
|
|
|
// Init sets a noop TracerProvider, optionally MeterProvider with OTLP HTTP exporter.
|
|
// Shutdown must be called on exit.
|
|
func Init(ctx context.Context, cfg Config) (shutdown func(context.Context) error, err error) {
|
|
if strings.TrimSpace(cfg.ServiceName) == "" {
|
|
return nil, errors.New("telemetry: ServiceName is required")
|
|
}
|
|
|
|
otel.SetTracerProvider(tracenoop.NewTracerProvider())
|
|
|
|
res, err := buildResource(ctx, cfg.ServiceName, cfg.ResourceAttributes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
mp *sdkmetric.MeterProvider
|
|
shutdownFuncs []func(context.Context) error
|
|
)
|
|
|
|
if metricsEnabled() {
|
|
opts := append(metricsClientOptions(),
|
|
otlpmetrichttp.WithTemporalitySelector(deltaTemporalitySelector),
|
|
)
|
|
mexp, err := otlpmetrichttp.New(ctx, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reader := sdkmetric.NewPeriodicReader(mexp)
|
|
mp = sdkmetric.NewMeterProvider(
|
|
sdkmetric.WithResource(res),
|
|
sdkmetric.WithReader(reader),
|
|
)
|
|
otel.SetMeterProvider(mp)
|
|
shutdownFuncs = append(shutdownFuncs, mp.Shutdown)
|
|
if cfg.RegisterMetrics != nil {
|
|
if err := cfg.RegisterMetrics(); err != nil {
|
|
_ = mp.Shutdown(ctx)
|
|
otel.SetMeterProvider(noop.NewMeterProvider())
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
shutdown = func(ctx context.Context) error {
|
|
var errs []error
|
|
for i := len(shutdownFuncs) - 1; i >= 0; i-- {
|
|
if err := shutdownFuncs[i](ctx); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|
|
return shutdown, nil
|
|
}
|
|
|
|
func buildResource(ctx context.Context, serviceName string, extra []attribute.KeyValue) (*resource.Resource, error) {
|
|
opts := []resource.Option{
|
|
resource.WithAttributes(semconv.ServiceName(serviceName)),
|
|
}
|
|
if len(extra) > 0 {
|
|
opts = append(opts, resource.WithAttributes(extra...))
|
|
}
|
|
return resource.New(ctx, opts...)
|
|
}
|
|
|
|
// Endpoint precedence: OTEL_EXPORTER_OTLP_*_ENDPOINT -> HOST_IP -> /etc/hostinfo.
|
|
func metricsEnabled() bool {
|
|
if otlpEndpointFromEnv() != "" {
|
|
return true
|
|
}
|
|
_, ok := resolveNodeIP()
|
|
return ok
|
|
}
|
|
|
|
func metricsClientOptions() []otlpmetrichttp.Option {
|
|
if otlpEndpointFromEnv() != "" {
|
|
return nil
|
|
}
|
|
ip, ok := resolveNodeIP()
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return []otlpmetrichttp.Option{
|
|
otlpmetrichttp.WithEndpoint(net.JoinHostPort(ip, otlpHTTPPort)),
|
|
otlpmetrichttp.WithInsecure(),
|
|
}
|
|
}
|
|
|
|
func otlpEndpointFromEnv() string {
|
|
return firstEndpoint(os.Getenv(envOTLPMetricsEndpoint), os.Getenv(envOTLPEndpoint))
|
|
}
|
|
|
|
func resolveNodeIP() (string, bool) {
|
|
if ip := strings.TrimSpace(os.Getenv(envHostIP)); ip != "" && net.ParseIP(ip) != nil {
|
|
return ip, true
|
|
}
|
|
return readHostInfoIP(hostInfoPath)
|
|
}
|
|
|
|
func readHostInfoIP(path string) (string, bool) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
if net.ParseIP(line) != nil {
|
|
return line, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func firstEndpoint(primary, fallback string) string {
|
|
if s := strings.TrimSpace(primary); s != "" {
|
|
return s
|
|
}
|
|
return strings.TrimSpace(fallback)
|
|
}
|
|
|
|
// deltaTemporalitySelector returns delta temporality for monotonic instruments
|
|
// (Counter, Histogram, ObservableCounter). Gauges and UpDownCounters keep
|
|
// the default cumulative semantics.
|
|
func deltaTemporalitySelector(kind sdkmetric.InstrumentKind) metricdata.Temporality {
|
|
switch kind {
|
|
case sdkmetric.InstrumentKindCounter,
|
|
sdkmetric.InstrumentKindHistogram:
|
|
return metricdata.DeltaTemporality
|
|
default:
|
|
return metricdata.CumulativeTemporality
|
|
}
|
|
}
|