Repository navigation
Expand file tree
/
Copy pathotlphttp.go
More file actions
208 lines (197 loc) · 8.89 KB
/
Copy pathotlphttp.go
File metadata and controls
208 lines (197 loc) · 8.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
package tracing
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
// OTLP/HTTP 的标准 signal 路径。
const (
otlpHTTPTracesPath = "/v1/traces"
otlpHTTPMetricsPath = "/v1/metrics"
)
// otlpHTTPEndpoint 是解析后的 OTLP/HTTP 端点。
type otlpHTTPEndpoint struct {
// hostPort "host:port",传给 WithEndpoint。
hostPort string
// basePath 基址路径前缀(不含 signal 段),如 "/api/public/otel"。
basePath string
// insecure 是否走 http:// 而非 https://。
insecure bool
}
// parseOTLPHTTPEndpoint 解析 OTLP/HTTP 端点。
//
// 接受三种写法(issue #23 的下游配置面就是这几种):
//
// 1. 完整 signal URL —— "https://host/api/public/otel/v1/traces":
// signal 段原样保留,另一个 signal 按同前缀换名推导;
// 2. 基址 —— "https://host" / "https://host/api/public/otel":
// 后接标准 signal 路径 /v1/traces 与 /v1/metrics;
// 3. 无 scheme 的 host:port —— "otel-collector:4318":
// scheme 由 Config.Insecure 决定。该形态不接受路径段:漏写 scheme 但
// 带基址路径("collector:4318/api/otel")直接报错并提示补 scheme(#112)
// ——此前路径段被整串吞进 host:port、basePath 恒空,导出打到根路径,
// /api/otel 段无声丢失(Init 成功、导出 404 循环),与 validateGRPCEndpoint
// 「不合规输入报错,不静默断供」的同一纪律相悖。
//
// 不接受 URL 内嵌凭据(http://user:pass@host):凭据走 Config.Headers——
// URL 里的凭据会进访问日志、代理日志与错误栈。
//
// 同样不接受 query 与 fragment(#99):此前二者被静默丢弃,query token 型
// 鉴权(?api_key= / ?token=)配置凭空消失——Init 成功、导出到后端却不带
// token,401/403 重试耗尽数据全丢,而配置里的 token「明明配了」。与内嵌
// 凭据同一纪律:显式报错并指向 Config.Headers(query 里的密钥同样会进
// 访问日志/代理日志);fragment 本来就不会发给服务端,出现即配置错误。
// 凭据/query/fragment 三道守卫对**无 scheme 形态同样生效**(#123):该形态
// 不走 url.Parse,此前全部绕过守卫——污染串原样进 hostPort,导出 URL 被
// 转义成畸形主机名(Init 成功、零报错、APM 全黑),或在导出器构造阶段报错
// 并把 token/凭据原样回显进错误信息。
func parseOTLPHTTPEndpoint(endpoint string, insecure bool) (otlpHTTPEndpoint, error) {
ep := otlpHTTPEndpoint{insecure: insecure}
raw := strings.TrimSpace(endpoint)
if raw == "" {
return ep, errors.New("tracing: empty OTLP/HTTP endpoint")
}
if !strings.Contains(raw, "://") {
if strings.Contains(raw, "/") {
// 漏写 scheme 且带路径:整串当 host:port 收下的话 basePath 恒空、
// 路径段无声丢失 ⇒ 显式报错让调用方补 scheme(#112)。错误不回显
// 端点原值——无 scheme 形态也可能内嵌凭据/令牌。
return ep, errors.New("tracing: scheme-less OTLP/HTTP endpoint must be plain \"host:port\" (e.g. \"otel-collector:4318\"); a base path requires an explicit scheme (e.g. \"https://host:4318/api/otel\")")
}
// 凭据/query/fragment 守卫(#123):与 scheme 分支(url.Parse 后的
// u.User/u.RawQuery/u.Fragment 三道)同口径,错误文案逐字一致且不
// 回显端点原值——无 scheme 形态同样可能内嵌凭据/令牌。
if strings.Contains(raw, "@") {
return ep, errors.New("tracing: OTLP/HTTP endpoint must not embed credentials; use Config.Headers instead")
}
if strings.Contains(raw, "?") {
return ep, errors.New("tracing: OTLP/HTTP endpoint must not carry query parameters; use Config.Headers instead")
}
if strings.Contains(raw, "#") {
return ep, errors.New("tracing: OTLP/HTTP endpoint must not carry a fragment; it is never sent to the server, use Config.Headers for auth")
}
// 无 scheme:host:port 形态,scheme 由 Insecure 决定。
ep.hostPort = raw
return ep, nil
}
u, err := url.Parse(raw)
if err != nil {
var ue *url.Error
if errors.As(err, &ue) {
// 只回显内层原因,不回显原始 URL(可能带凭据/令牌)。
return ep, fmt.Errorf("tracing: invalid OTLP/HTTP endpoint: %w", ue.Err)
}
return ep, errors.New("tracing: invalid OTLP/HTTP endpoint")
}
switch strings.ToLower(u.Scheme) {
case "http":
ep.insecure = true
case "https":
ep.insecure = false
default:
return ep, fmt.Errorf("tracing: unsupported OTLP/HTTP endpoint scheme %q (want http or https)", u.Scheme)
}
if u.User != nil {
return ep, errors.New("tracing: OTLP/HTTP endpoint must not embed credentials; use Config.Headers instead")
}
if u.RawQuery != "" {
// 不回显原值:query 里可能就是 token 本体。
return ep, errors.New("tracing: OTLP/HTTP endpoint must not carry query parameters; use Config.Headers instead")
}
if u.Fragment != "" {
return ep, errors.New("tracing: OTLP/HTTP endpoint must not carry a fragment; it is never sent to the server, use Config.Headers for auth")
}
if u.Host == "" {
return ep, errors.New("tracing: OTLP/HTTP endpoint missing host")
}
ep.hostPort = u.Host
ep.basePath = strings.TrimSuffix(u.Path, "/")
return ep, nil
}
// signalPaths 推导 traces / metrics 的 URL 路径。
//
// 端点已指到某个 signal(以 /v1/traces 或 /v1/metrics 结尾)→ 该 signal 原样用,
// 另一个按同前缀换名;否则基址后拼标准 signal 路径。
func (e otlpHTTPEndpoint) signalPaths() (traces, metrics string) {
base := e.basePath
switch {
case strings.HasSuffix(base, otlpHTTPTracesPath):
return base, strings.TrimSuffix(base, otlpHTTPTracesPath) + otlpHTTPMetricsPath
case strings.HasSuffix(base, otlpHTTPMetricsPath):
return strings.TrimSuffix(base, otlpHTTPMetricsPath) + otlpHTTPTracesPath, base
default:
return base + otlpHTTPTracesPath, base + otlpHTTPMetricsPath
}
}
// newHTTPExportingProviders 创建带 OTLP/HTTP 导出器的 Tracer/Meter Provider。
//
// 与 grpc 分支**同样的全局语义**:MeterProvider(含 Go runtime 指标)一并装上,
// 传播器由 Init 统一设置,被替换的旧 provider 由 Init 统一关闭——
// 「换协议」退回「换一条传输」,而不是「换一整套可观测性语义」。
//
// 此前 OTLP/HTTP 只能由下游绕开 tracing.Init 自写整套装配,漏掉传播器/
// 指标/旧 provider 关闭时全是**静默**缺口(链路断但不报错)。
func newHTTPExportingProviders(cfg Config, sampler sdktrace.Sampler, res *resource.Resource) (*sdktrace.TracerProvider, *metric.MeterProvider, error) {
ep, err := parseOTLPHTTPEndpoint(cfg.Endpoint, cfg.Insecure)
if err != nil {
return nil, nil, err
}
tracePath, metricPath := ep.signalPaths()
// ---- tracing exporter ----
var traceOpts []otlptracehttp.Option
traceOpts = append(traceOpts,
otlptracehttp.WithEndpoint(ep.hostPort),
otlptracehttp.WithURLPath(tracePath),
)
if ep.insecure {
traceOpts = append(traceOpts, otlptracehttp.WithInsecure())
}
if len(cfg.Headers) > 0 {
traceOpts = append(traceOpts, otlptracehttp.WithHeaders(cfg.Headers))
}
traceExp, err := otlptrace.New(context.Background(), otlptracehttp.NewClient(traceOpts...))
if err != nil {
return nil, nil, fmt.Errorf("tracing: create otlp http trace exporter: %w", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sampler),
sdktrace.WithResource(res),
sdktrace.WithSpanProcessor(sdktrace.NewBatchSpanProcessor(traceExp)),
)
// ---- metrics exporter(与 grpc 分支等价)----
// ⭐ DisableMetrics=true ⇒ 不建 metrics 导出器(与 grpc 分支同一纪律):
// collector 侧无 metrics 管线时推送只会每 15s 换一条 Unimplemented 报错,
// 且这些指标本就无处可去 ⇒ 关掉即止噪,待配好 metrics 管线后改回 false 即可恢复。
if cfg.DisableMetrics {
return tp, nil, nil
}
var metricOpts []otlpmetrichttp.Option
metricOpts = append(metricOpts,
otlpmetrichttp.WithEndpoint(ep.hostPort),
otlpmetrichttp.WithURLPath(metricPath),
)
if ep.insecure {
metricOpts = append(metricOpts, otlpmetrichttp.WithInsecure())
}
if len(cfg.Headers) > 0 {
metricOpts = append(metricOpts, otlpmetrichttp.WithHeaders(cfg.Headers))
}
metricExp, err := otlpmetrichttp.New(context.Background(), metricOpts...)
if err != nil {
_ = tp.Shutdown(context.Background()) // 回滚已创建的 trace provider
return nil, nil, fmt.Errorf("tracing: create otlp http metric exporter: %w", err)
}
mp, err := assembleMeterProvider(metricExp, res, tp) // 装配尾段与 grpc 分支共用(#110)
if err != nil {
return nil, nil, err
}
return tp, mp, nil
}