OpenTelemetry-Go:MeterProvider跨包使用失效问题咨询
问题
我按照OpenTelemetry文档实现指标功能,把MeterProvider的初始化代码封装到了独立的other包中。在main包通过otel.Meter()获取Meter并构建计数器调用.Add()后,发现该指标无法输出到stdout exporter,只有other包内生成的指标能正常导出。
我的目标是将OpenTelemetry的构建与初始化代码独立封装,方便在main包中调用并获取Meter。想请教三个问题:
- 是否支持跨包使用其他包构建的Meter/MeterProvider?
- 这种跨包封装的方式是否为最佳实践?
- 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

