Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

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
}
}