You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OpenTelemetry-Go:MeterProvider跨包使用失效问题咨询

问题

我按照OpenTelemetry文档实现指标功能,把MeterProvider的初始化代码封装到了独立的other包中。在main包通过otel.Meter()获取Meter并构建计数器调用.Add()后,发现该指标无法输出到stdout exporter,只有other包内生成的指标能正常导出。

我的目标是将OpenTelemetry的构建与初始化代码独立封装,方便在main包中调用并获取Meter。想请教三个问题:

  1. 是否支持跨包使用其他包构建的Meter/MeterProvider?
  2. 这种跨包封装的方式是否为最佳实践?
  3. MeterProvider是否必须在同一包中构建?

代码示例

main包代码

package main

import (
    "context"
    "localtest/other"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    otm "go.opentelemetry.io/otel/metric"
)

func main() {
    other.Test()

    mainMeter := otel.GetMeterProvider().Meter("localtest/other/myTestMeter")

    mainCounter, _ := mainMeter.Int64Counter(
        "temporary.call",
        otm.WithUnit("{call}"),
        otm.WithDescription("car counter"),
    )
    mainCounter.Add(context.Background(), 1, otm.WithAttributes(
        attribute.String("model", "Camaro"),
        attribute.Bool("IsCar", true),
    ))

}

other包代码

package other

import (
    "context"

    "log"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
    otm "go.opentelemetry.io/otel/metric"
    "go.opentelemetry.io/otel/sdk/metric"
    "go.opentelemetry.io/otel/sdk/resource"
    semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

func Test() {
    res, err := newResource()
    if err != nil {
        panic(err)
    }
    meterProvider, err := newMeterProvider(res)
    if err != nil {
        panic(err)
    }

    defer func() {
        if err := meterProvider.Shutdown(context.Background()); err != nil {
            log.Println(err)
        }
    }()

    otel.SetMeterProvider(meterProvider)

    Meter := otel.Meter("myTestMeter")

    otherCounter, err := Meter.Int64Counter(
        "temporary.call",
        otm.WithUnit("{call}"),
        otm.WithDescription("car counter"),
    )
    otherCounter.Add(context.Background(), 1, otm.WithAttributes(
        attribute.String("model", "Monza"),
        attribute.Bool("IsCar", true),
    ))

}

func newResource() (*resource.Resource, error) {
    return resource.Merge(resource.Default(),
        resource.NewWithAttributes(semconv.SchemaURL,
            semconv.ServiceName("my-service"),
            semconv.ServiceVersion("0.1.0"),
        ))
}

func newMeterProvider(res *resource.Resource) (*metric.MeterProvider, error) {
    metricExporter, err := stdoutmetric.New()
    if err != nil {
        return nil, err
    }

    meterProvider := metric.NewMeterProvider(
        metric.WithResource(res),
        metric.WithReader(metric.NewPeriodicReader(metricExporter,
            // Default is 1m. Set to 3s for demonstrative purposes.
            metric.WithInterval(3*time.Second))),
    )
    return meterProvider, nil
}

当前输出结果

仅包含other包生成的指标:

{"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"my-service"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"0.1.0"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.32.0"}}],"ScopeMetrics":[{"Scope":{"Name":"myTestMeter","Version":"","SchemaURL":"","Attributes":null},"Metrics":[{"Name":"temporary.call","Description":"car counter","Unit":"{call}","Data":{"DataPoints":[{"Attributes":[{"Key":"IsCar","Value":{"Type":"BOOL","Value":true}},{"Key":"model","Value":{"Type":"STRING","Value":"Monza"}}],"StartTime":"2024-11-20T07:40:23.831377979-03:00","Time":"2024-11-20T07:40:23.831517987-03:00","Value":1}],"Temporality":"CumulativeTemporality","IsMonotonic":true}}]}]}

解答

1. 是否支持跨包使用其他包构建的Meter/MeterProvider?

完全支持。OpenTelemetry的全局otel包提供了SetMeterProvider和GetMeterProvider方法,就是为了让你可以在任意包中设置全局的MeterProvider,然后在其他包中获取并使用它。

你代码中的问题不是跨包导致的,而是生命周期管理错误:在other.Test()函数中,你用defer调用了meterProvider.Shutdown(),当Test()执行完毕返回时,这个defer会立即触发关闭MeterProvider。此时main函数才刚创建计数器并调用Add(),但MeterProvider已经关闭,无法再收集和导出指标。

2. 跨包封装初始化代码是否为最佳实践?

是最佳实践之一。将OpenTelemetry的初始化(包括Resource、Exporter、MeterProvider的创建)封装到独立的工具包中,能避免业务代码重复初始化逻辑,保持代码整洁,也方便统一管理遥测配置。

需要注意两个点:

  • 初始化逻辑要在程序启动时尽早执行,确保MeterProvider的生命周期与程序一致,不要过早关闭。
  • 建议将初始化后的MeterProvider返回给调用者(比如main包),由调用者负责管理生命周期(比如在程序退出时调用Shutdown),而不是在封装函数内部用defer关闭。

3. MeterProvider是否必须在同一包中构建?

不需要。MeterProvider可以在任意包中构建,只要通过otel.SetMeterProvider()设置为全局实例,或者直接传递MeterProvider实例到其他包使用,都能正常工作。直接传递实例的方式甚至比全局实例更可控,适合依赖注入场景。


修复代码示例

修改other包的初始化函数,返回MeterProvider由main包管理生命周期:

修改后的other包代码

package other

import (
    "context"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
    otm "go.opentelemetry.io/otel/metric"
    "go.opentelemetry.io/otel/sdk/metric"
    "go.opentelemetry.io/otel/sdk/resource"
    semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

// 初始化指标并返回MeterProvider,由调用者管理关闭
func InitMetrics() (*metric.MeterProvider, error) {
    res, err := newResource()
    if err != nil {
        return nil, err
    }
    meterProvider, err := newMeterProvider(res)
    if err != nil {
        return nil, err
    }

    otel.SetMeterProvider(meterProvider)

    // 初始化包内指标
    meter := otel.Meter("myTestMeter")
    otherCounter, err := meter.Int64Counter(
        "temporary.call",
        otm.WithUnit("{call}"),
        otm.WithDescription("car counter"),
    )
    if err != nil {
        return nil, err
    }
    otherCounter.Add(context.Background(), 1, otm.WithAttributes(
        attribute.String("model", "Monza"),
        attribute.Bool("IsCar", true),
    ))

    return meterProvider, nil
}

func newResource() (*resource.Resource, error) {
    return resource.Merge(resource.Default(),
        resource.NewWithAttributes(semconv.SchemaURL,
            semconv.ServiceName("my-service"),
            semconv.ServiceVersion("0.1.0"),
        ))
}

func newMeterProvider(res *resource.Resource) (*metric.MeterProvider, error) {
    metricExporter, err := stdoutmetric.New()
    if err != nil {
        return nil, err
    }

    return metric.NewMeterProvider(
        metric.WithResource(res),
        metric.WithReader(metric.NewPeriodicReader(metricExporter,
            metric.WithInterval(3*time.Second))),
    ), nil
}

修改后的main包代码

package main

import (
    "context"
    "localtest/other"
    "log"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    otm "go.opentelemetry.io/otel/metric"
)

func main() {
    meterProvider, err := other.InitMetrics()
    if err != nil {
        panic(err)
    }

    // 等待指标导出周期完成后再关闭MeterProvider
    defer func() {
        time.Sleep(3 * time.Second)
        if err := meterProvider.Shutdown(context.Background()); err != nil {
            log.Println(err)
        }
    }()

    mainMeter := otel.GetMeterProvider().Meter("localtest/other/myTestMeter")

    mainCounter, err := mainMeter.Int64Counter(
        "temporary.call",
        otm.WithUnit("{call}"),
        otm.WithDescription("car counter"),
    )
    if err != nil {
        panic(err)
    }
    mainCounter.Add(context.Background(), 1, otm.WithAttributes(
        attribute.String("model", "Camaro"),
        attribute.Bool("IsCar", true),
    ))
}

修改后,main包中的指标会被正常导出,因为MeterProvider直到程序退出时才会关闭,足够时间让周期性导出器收集并输出指标。


内容的提问来源于stack exchange,提问作者ramon perez

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 01:02:04