feat: 增强 OpenTelemetry 提供者配置,添加连接安全、采样器和批处理选项
This commit is contained in:
@@ -458,3 +458,56 @@ import (
|
|||||||
|
|
||||||
// 仅需 import 即可,无额外 ServerOption
|
// 仅需 import 即可,无额外 ServerOption
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## OpenTelemetry 集成(推荐)
|
||||||
|
|
||||||
|
使用 StatsHandler(推荐,不与拦截器同时使用,避免重复埋点):
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
otelgrpc "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 基本接入:使用全局 Tracer/Meter(由 OTEL Provider 初始化)
|
||||||
|
handler := otelgrpc.NewServerHandler(
|
||||||
|
otelgrpc.WithTraceEvents(), // 在 span 中记录消息事件
|
||||||
|
)
|
||||||
|
pgrpc.UseOptions(grpc.StatsHandler(handler))
|
||||||
|
|
||||||
|
// 忽略某些方法(如健康检查),避免噪声:
|
||||||
|
handler = otelgrpc.NewServerHandler(
|
||||||
|
otelgrpc.WithFilter(func(ctx context.Context, fullMethod string) bool {
|
||||||
|
return fullMethod != "/grpc.health.v1.Health/Check"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
pgrpc.UseOptions(grpc.StatsHandler(handler))
|
||||||
|
```
|
||||||
|
|
||||||
|
使用拦截器版本(如你更偏好 Interceptor 方案;与 StatsHandler 二选一):
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
otelgrpc "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
pgrpc.UseUnaryInterceptors(otelgrpc.UnaryServerInterceptor())
|
||||||
|
pgrpc.UseStreamInterceptors(otelgrpc.StreamServerInterceptor())
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注意:不要同时启用 StatsHandler 和拦截器,否则会重复生成 span/metrics。
|
||||||
|
|
||||||
|
## OpenTracing(Jaeger)集成
|
||||||
|
|
||||||
|
当使用 Tracing Provider(Jaeger + OpenTracing)时,可使用 opentracing 的 gRPC 拦截器:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
opentracing "github.com/opentracing/opentracing-go"
|
||||||
|
otgrpc "github.com/grpc-ecosystem/grpc-opentracing/go/otgrpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
pgrpc.UseUnaryInterceptors(otgrpc.OpenTracingServerInterceptor(opentracing.GlobalTracer()))
|
||||||
|
pgrpc.UseStreamInterceptors(otgrpc.OpenTracingStreamServerInterceptor(opentracing.GlobalTracer()))
|
||||||
|
```
|
||||||
|
|
||||||
|
> 与 OTEL 方案互斥:如果已启用 OTEL,请不要再开启 OpenTracing 拦截器,以免重复埋点。
|
||||||
|
|||||||
121
templates/project/providers/otel/README.md.raw
Normal file
121
templates/project/providers/otel/README.md.raw
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
OpenTelemetry Provider (OTLP Traces + Metrics)
|
||||||
|
|
||||||
|
该 Provider 基于 OpenTelemetry Go SDK,初始化全局 Tracer 与 Meter,支持 OTLP(gRPC/HTTP) 导出,并收集运行时指标。
|
||||||
|
|
||||||
|
配置(config.toml)
|
||||||
|
|
||||||
|
```
|
||||||
|
[OTEL]
|
||||||
|
ServiceName = "my-service"
|
||||||
|
Version = "1.0.0"
|
||||||
|
Env = "dev"
|
||||||
|
|
||||||
|
# 导出端点(二选一)
|
||||||
|
EndpointGRPC = "otel-collector:4317"
|
||||||
|
EndpointHTTP = "otel-collector:4318"
|
||||||
|
|
||||||
|
# 认证(可选)
|
||||||
|
Token = "Bearer <your-token>" # 也可只填纯 token,Provider 会自动补齐 Bearer 前缀
|
||||||
|
|
||||||
|
# 安全(可选)
|
||||||
|
InsecureGRPC = true # gRPC 导出是否使用 insecure
|
||||||
|
InsecureHTTP = true # HTTP 导出是否使用 insecure
|
||||||
|
|
||||||
|
# 采样(可选)
|
||||||
|
Sampler = "always" # always|ratio
|
||||||
|
SamplerRatio = 0.1 # Sampler=ratio 时生效,0..1
|
||||||
|
|
||||||
|
# 批处理(可选,毫秒)
|
||||||
|
BatchTimeoutMs = 5000
|
||||||
|
ExportTimeoutMs = 10000
|
||||||
|
MaxQueueSize = 2048
|
||||||
|
MaxExportBatchSize = 512
|
||||||
|
|
||||||
|
# 指标(可选,毫秒)
|
||||||
|
MetricReaderIntervalMs = 10000 # 指标导出周期
|
||||||
|
RuntimeReadMemStatsIntervalMs = 5000 # 运行时指标读取周期
|
||||||
|
```
|
||||||
|
|
||||||
|
启用
|
||||||
|
|
||||||
|
```
|
||||||
|
import "test/providers/otel"
|
||||||
|
|
||||||
|
func providers() container.Providers {
|
||||||
|
return container.Providers{
|
||||||
|
otel.DefaultProvider(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
使用
|
||||||
|
|
||||||
|
- Traces: 通过 `go.opentelemetry.io/otel` 获取全局 Tracer,或使用仓库提供的 `providers/otel/funcs.go` 包装。
|
||||||
|
|
||||||
|
```
|
||||||
|
ctx, span := otel.Tracer("my-service").Start(ctx, "my-op")
|
||||||
|
// ...
|
||||||
|
span.End()
|
||||||
|
```
|
||||||
|
|
||||||
|
- Metrics: 通过 `otel.Meter("my-service")` 创建仪表,或使用 `providers/otel/funcs.go` 的便捷函数。
|
||||||
|
|
||||||
|
与 Tracing Provider 的区别与场景建议
|
||||||
|
|
||||||
|
- Tracing Provider(Jaeger + OpenTracing)只做链路,适合已有 OpenTracing 项目;
|
||||||
|
- OTEL Provider(OpenTelemetry)统一 Traces+Metrics,对接 OTLP 生态,适合新项目或希望统一可观测性;
|
||||||
|
- 可先混用:保留 Jaeger 链路,同时启用 OTEL 运行时指标,逐步迁移。
|
||||||
|
|
||||||
|
快速启动(本地 Collector)
|
||||||
|
|
||||||
|
最小化 docker-compose:
|
||||||
|
|
||||||
|
```
|
||||||
|
services:
|
||||||
|
otel-collector:
|
||||||
|
image: otel/opentelemetry-collector:0.104.0
|
||||||
|
command: ["--config=/etc/otelcol-config.yml"]
|
||||||
|
volumes:
|
||||||
|
- ./otelcol-config.yml:/etc/otelcol-config.yml:ro
|
||||||
|
ports:
|
||||||
|
- "4317:4317" # OTLP gRPC
|
||||||
|
- "4318:4318" # OTLP HTTP
|
||||||
|
```
|
||||||
|
|
||||||
|
示例 otelcol-config.yml:
|
||||||
|
|
||||||
|
```
|
||||||
|
receivers:
|
||||||
|
otlp:
|
||||||
|
protocols:
|
||||||
|
grpc:
|
||||||
|
http:
|
||||||
|
exporters:
|
||||||
|
debug:
|
||||||
|
verbosity: detailed
|
||||||
|
processors:
|
||||||
|
batch:
|
||||||
|
service:
|
||||||
|
pipelines:
|
||||||
|
traces:
|
||||||
|
receivers: [otlp]
|
||||||
|
processors: [batch]
|
||||||
|
exporters: [debug]
|
||||||
|
metrics:
|
||||||
|
receivers: [otlp]
|
||||||
|
processors: [batch]
|
||||||
|
exporters: [debug]
|
||||||
|
```
|
||||||
|
|
||||||
|
应用端:
|
||||||
|
|
||||||
|
```
|
||||||
|
[OTEL]
|
||||||
|
EndpointGRPC = "127.0.0.1:4317"
|
||||||
|
InsecureGRPC = true
|
||||||
|
```
|
||||||
|
|
||||||
|
故障与降级
|
||||||
|
|
||||||
|
- Collector/网络异常:OTEL SDK 异步批处理,不阻塞业务;可能丢点/丢指标;
|
||||||
|
- 启动失败:初始化报错会阻止启动;如需“不可达也不影响启动”,可加开关降级为 no-op(可按需补充)。
|
||||||
@@ -28,6 +28,25 @@ type Config struct {
|
|||||||
EndpointGRPC string
|
EndpointGRPC string
|
||||||
EndpointHTTP string
|
EndpointHTTP string
|
||||||
Token string
|
Token string
|
||||||
|
|
||||||
|
// Connection security
|
||||||
|
InsecureGRPC bool // if true, use grpc insecure for OTLP gRPC
|
||||||
|
InsecureHTTP bool // if true, use http insecure for OTLP HTTP
|
||||||
|
|
||||||
|
// Tracing sampler
|
||||||
|
// Sampler: "always" (default) or "ratio"
|
||||||
|
Sampler string
|
||||||
|
SamplerRatio float64 // used when Sampler == "ratio"; 0..1
|
||||||
|
|
||||||
|
// Tracing batcher options (milliseconds)
|
||||||
|
BatchTimeoutMs uint
|
||||||
|
ExportTimeoutMs uint
|
||||||
|
MaxQueueSize int
|
||||||
|
MaxExportBatchSize int
|
||||||
|
|
||||||
|
// Metrics options (milliseconds)
|
||||||
|
MetricReaderIntervalMs uint // export interval for PeriodicReader
|
||||||
|
RuntimeReadMemStatsIntervalMs uint // runtime metrics min read interval
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) format() {
|
func (c *Config) format() {
|
||||||
|
|||||||
@@ -26,6 +26,17 @@ import (
|
|||||||
"google.golang.org/grpc/encoding/gzip"
|
"google.golang.org/grpc/encoding/gzip"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// formatAuth formats token into an Authorization header value.
|
||||||
|
func formatAuth(token string) string {
|
||||||
|
if token == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(token) > 7 && (token[:7] == "Bearer " || token[:7] == "bearer ") {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
return "Bearer " + token
|
||||||
|
}
|
||||||
|
|
||||||
func Provide(opts ...opt.Option) error {
|
func Provide(opts ...opt.Option) error {
|
||||||
o := opt.New(opts...)
|
o := opt.New(opts...)
|
||||||
var config Config
|
var config Config
|
||||||
@@ -82,7 +93,7 @@ func (o *builder) initResource(ctx context.Context) (err error) {
|
|||||||
semconv.HostNameKey.String(hostName), // 主机名
|
semconv.HostNameKey.String(hostName), // 主机名
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *builder) initMeterProvider(ctx context.Context) (err error) {
|
func (o *builder) initMeterProvider(ctx context.Context) (err error) {
|
||||||
@@ -92,9 +103,8 @@ func (o *builder) initMeterProvider(ctx context.Context) (err error) {
|
|||||||
otlpmetricgrpc.WithCompressor(gzip.Name),
|
otlpmetricgrpc.WithCompressor(gzip.Name),
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.config.Token != "" {
|
if h := formatAuth(o.config.Token); h != "" {
|
||||||
headers := map[string]string{"Authentication": o.config.Token}
|
opts = append(opts, otlpmetricgrpc.WithHeaders(map[string]string{"Authorization": h}))
|
||||||
opts = append(opts, otlpmetricgrpc.WithHeaders(headers))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exporter, err := otlpmetricgrpc.New(ctx, opts...)
|
exporter, err := otlpmetricgrpc.New(ctx, opts...)
|
||||||
@@ -109,9 +119,11 @@ func (o *builder) initMeterProvider(ctx context.Context) (err error) {
|
|||||||
otlpmetrichttp.WithEndpoint(o.config.EndpointHTTP),
|
otlpmetrichttp.WithEndpoint(o.config.EndpointHTTP),
|
||||||
otlpmetrichttp.WithCompression(1),
|
otlpmetrichttp.WithCompression(1),
|
||||||
}
|
}
|
||||||
|
if o.config.InsecureHTTP {
|
||||||
if o.config.Token != "" {
|
opts = append(opts, otlpmetrichttp.WithInsecure())
|
||||||
opts = append(opts, otlpmetrichttp.WithURLPath(o.config.Token))
|
}
|
||||||
|
if h := formatAuth(o.config.Token); h != "" {
|
||||||
|
opts = append(opts, otlpmetrichttp.WithHeaders(map[string]string{"Authorization": h}))
|
||||||
}
|
}
|
||||||
|
|
||||||
exporter, err := otlpmetrichttp.New(ctx, opts...)
|
exporter, err := otlpmetrichttp.New(ctx, opts...)
|
||||||
@@ -129,18 +141,27 @@ func (o *builder) initMeterProvider(ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// periodic reader with optional custom interval
|
||||||
|
var readerOpts []sdkmetric.PeriodicReaderOption
|
||||||
|
if o.config.MetricReaderIntervalMs > 0 {
|
||||||
|
readerOpts = append(readerOpts, sdkmetric.WithInterval(time.Duration(o.config.MetricReaderIntervalMs)*time.Millisecond))
|
||||||
|
}
|
||||||
meterProvider := sdkmetric.NewMeterProvider(
|
meterProvider := sdkmetric.NewMeterProvider(
|
||||||
sdkmetric.WithReader(
|
sdkmetric.WithReader(
|
||||||
sdkmetric.NewPeriodicReader(exporter),
|
sdkmetric.NewPeriodicReader(exporter, readerOpts...),
|
||||||
),
|
),
|
||||||
sdkmetric.WithResource(o.resource),
|
sdkmetric.WithResource(o.resource),
|
||||||
)
|
)
|
||||||
otel.SetMeterProvider(meterProvider)
|
otel.SetMeterProvider(meterProvider)
|
||||||
|
|
||||||
err = runtime.Start(runtime.WithMinimumReadMemStatsInterval(time.Second * 5))
|
interval := 5 * time.Second
|
||||||
|
if o.config.RuntimeReadMemStatsIntervalMs > 0 {
|
||||||
|
interval = time.Duration(o.config.RuntimeReadMemStatsIntervalMs) * time.Millisecond
|
||||||
|
}
|
||||||
|
err = runtime.Start(runtime.WithMinimumReadMemStatsInterval(interval))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "Failed to start runtime metrics")
|
return errors.Wrapf(err, "Failed to start runtime metrics")
|
||||||
}
|
}
|
||||||
@@ -151,7 +172,7 @@ func (o *builder) initMeterProvider(ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *builder) initTracerProvider(ctx context.Context) error {
|
func (o *builder) initTracerProvider(ctx context.Context) error {
|
||||||
@@ -159,15 +180,13 @@ func (o *builder) initTracerProvider(ctx context.Context) error {
|
|||||||
opts := []otlptracegrpc.Option{
|
opts := []otlptracegrpc.Option{
|
||||||
otlptracegrpc.WithCompressor(gzip.Name),
|
otlptracegrpc.WithCompressor(gzip.Name),
|
||||||
otlptracegrpc.WithEndpoint(o.config.EndpointGRPC),
|
otlptracegrpc.WithEndpoint(o.config.EndpointGRPC),
|
||||||
otlptracegrpc.WithInsecure(), // 添加不安全连接选项
|
}
|
||||||
|
if o.config.InsecureGRPC {
|
||||||
|
opts = append(opts, otlptracegrpc.WithInsecure())
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.config.Token != "" {
|
if h := formatAuth(o.config.Token); h != "" {
|
||||||
headers := map[string]string{
|
opts = append(opts, otlptracegrpc.WithHeaders(map[string]string{"Authorization": h}))
|
||||||
"Authentication": o.config.Token,
|
|
||||||
"authorization": o.config.Token, // 添加标准认证头
|
|
||||||
}
|
|
||||||
opts = append(opts, otlptracegrpc.WithHeaders(headers))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Creating GRPC trace exporter with endpoint: %s", o.config.EndpointGRPC)
|
log.Debugf("Creating GRPC trace exporter with endpoint: %s", o.config.EndpointGRPC)
|
||||||
@@ -189,17 +208,15 @@ func (o *builder) initTracerProvider(ctx context.Context) error {
|
|||||||
|
|
||||||
exporterHttpFunc := func(ctx context.Context) (*otlptrace.Exporter, error) {
|
exporterHttpFunc := func(ctx context.Context) (*otlptrace.Exporter, error) {
|
||||||
opts := []otlptracehttp.Option{
|
opts := []otlptracehttp.Option{
|
||||||
otlptracehttp.WithInsecure(),
|
|
||||||
otlptracehttp.WithCompression(1),
|
otlptracehttp.WithCompression(1),
|
||||||
otlptracehttp.WithEndpoint(o.config.EndpointHTTP),
|
otlptracehttp.WithEndpoint(o.config.EndpointHTTP),
|
||||||
}
|
}
|
||||||
|
if o.config.InsecureHTTP {
|
||||||
|
opts = append(opts, otlptracehttp.WithInsecure())
|
||||||
|
}
|
||||||
|
|
||||||
if o.config.Token != "" {
|
if h := formatAuth(o.config.Token); h != "" {
|
||||||
opts = append(opts,
|
opts = append(opts, otlptracehttp.WithHeaders(map[string]string{"Authorization": h}))
|
||||||
otlptracehttp.WithHeaders(map[string]string{
|
|
||||||
"Authentication": o.config.Token,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Creating HTTP trace exporter with endpoint: %s", o.config.EndpointHTTP)
|
log.Debugf("Creating HTTP trace exporter with endpoint: %s", o.config.EndpointHTTP)
|
||||||
@@ -225,10 +242,38 @@ func (o *builder) initTracerProvider(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sampler
|
||||||
|
sampler := sdktrace.AlwaysSample()
|
||||||
|
if o.config.Sampler == "ratio" {
|
||||||
|
ratio := o.config.SamplerRatio
|
||||||
|
if ratio <= 0 {
|
||||||
|
ratio = 0
|
||||||
|
}
|
||||||
|
if ratio > 1 {
|
||||||
|
ratio = 1
|
||||||
|
}
|
||||||
|
sampler = sdktrace.ParentBased(sdktrace.TraceIDRatioBased(ratio))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batcher options
|
||||||
|
var batchOpts []sdktrace.BatchSpanProcessorOption
|
||||||
|
if o.config.BatchTimeoutMs > 0 {
|
||||||
|
batchOpts = append(batchOpts, sdktrace.WithBatchTimeout(time.Duration(o.config.BatchTimeoutMs)*time.Millisecond))
|
||||||
|
}
|
||||||
|
if o.config.ExportTimeoutMs > 0 {
|
||||||
|
batchOpts = append(batchOpts, sdktrace.WithExportTimeout(time.Duration(o.config.ExportTimeoutMs)*time.Millisecond))
|
||||||
|
}
|
||||||
|
if o.config.MaxQueueSize > 0 {
|
||||||
|
batchOpts = append(batchOpts, sdktrace.WithMaxQueueSize(o.config.MaxQueueSize))
|
||||||
|
}
|
||||||
|
if o.config.MaxExportBatchSize > 0 {
|
||||||
|
batchOpts = append(batchOpts, sdktrace.WithMaxExportBatchSize(o.config.MaxExportBatchSize))
|
||||||
|
}
|
||||||
|
|
||||||
traceProvider := sdktrace.NewTracerProvider(
|
traceProvider := sdktrace.NewTracerProvider(
|
||||||
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
sdktrace.WithSampler(sampler),
|
||||||
sdktrace.WithResource(o.resource),
|
sdktrace.WithResource(o.resource),
|
||||||
sdktrace.WithBatcher(exporter),
|
sdktrace.WithBatcher(exporter, batchOpts...),
|
||||||
)
|
)
|
||||||
container.AddCloseAble(func() {
|
container.AddCloseAble(func() {
|
||||||
log.Error("shut down")
|
log.Error("shut down")
|
||||||
|
|||||||
Reference in New Issue
Block a user