探索 Go 语言中 Opentelemetry 与 Prometheus 集成,导出 HTTP 服务指标监控,并最终将 Prometheus 指标可视化到 Grafana 中。
学习目标
学完本章你应该能够:
- 讲清 OpenTelemetry Metrics 的核心抽象链:
MeterProvider→Meter→Instrument→Measurements,以及Metric Reader/Metric Exporter在其中的角色。 - 区分 Counter / UpDownCounter / Gauge / Histogram 四种指标类型的语义,知道"只增不减"和"瞬时值"分别该用哪个。
- 用 Go 代码跑通"创建一个
Int64Counter,在 HTTP handler 里Add(1),再通过/metrics暴露"的最小链路。 - 理解 Histogram 用"显式桶边界"做分位统计的原理,能给
request_hello_duration_seconds自定义桶。 - 把数据接入 Prometheus(拉取
/metrics)和 Grafana(数据源 + Dashboard)做可视化,讲清"应用 → Exporter → Prometheus → Grafana"的流向。
前置知识:
- Go 语言基础(
package main、HTTP handler、context) - 一点 Prometheus 概念(它会主动"拉"你的
/metrics端点) - Docker 基础(文末用 Docker 起 Prometheus / Grafana)
本章你会动手做的事:
go get三个依赖,把"请求计数"的 Counter 跑起来,访问/index看requests_hello_total增长。- 加一个 Histogram,用
rand模拟耗时,观察/metrics里_bucket/_sum/_count三件套。 - 用 Docker 起 Prometheus + Grafana,把
requests_hello_total画成一张图。
前言
Opentelemetry
分布式链路跟踪( Distributed Tracing )的概念最早是由 Google 提出来的,发展至今技术已经比较成熟,也是有一些协议标准可以参考。目前在 Tracing 技术这块比较有影响力的是两大开源技术框架:Netflix 公司开源的 OpenTracing 和 Google 开源的 OpenCensus。两大框架都拥有比较高的开发者群体。为形成统一的技术标准,两大框架最终磨合成立了 OpenTelemetry 项目,简称 otel。
Prometheus
Prometheus 源自 SoundCloud,拥有一整套开源系统监控和警报工具包,是支持 OpenTelemetry 的系统之一,是 CNCF
的第二个项目
Grafana
Grafana 是一个开源的分析和可视化平台,它允许你查询、可视化和警报来自各种数据源的数据。它提供了一个用户友好的界面,用于创建和共享仪表板、图表和警报。Grafana 支持广泛的数据源,其中就包括 Prometheus

基础概念
这里为了简单入门,尽量简单的介绍一些抽象概念,结合着代码理解,如果不能理解也没关系,代码写着写着自然就明白了:
MeterProvider 是"厂长",负责统筹所有生产线;Meter 是"某条生产线"(比如 HTTP 线、MySQL 线),每条线管一类组件;Instrument 是生产线上的"具体仪表"(计数器、温度计);Measurements 是仪表上实时跳动的读数(数据点)。Metric Reader 是"巡检员",定期去读数;Metric Exporter 是"对外汇报员",把读数交给 Prometheus 这类外部系统。下面这张图把这套层级关系画清楚:
flowchart TB
MP[MeterProvider
指标工厂/厂长] --> M1[Meter: HTTP 组件]
MP --> M2[Meter: MySQL 组件]
M1 --> I1[Instrument: 请求计数]
M1 --> I2[Instrument: 请求耗时]
M2 --> I3[Instrument: 连接数]
I1 --> D[(Measurements
数据点)]
I2 --> D
I3 --> D
MP --> MR[Metric Reader
巡检员读数]
MR --> ME[Metric Exporter
汇报给 Prometheus]Meter Provider
用于接口化管理全局的 Meter 创建,相当于全局的监控指标管理工厂。
Meter
用于接口化创建并管理全局的 Instrument,不同的 Meter 可以看做是不同的程序组件。
Instrument
用于管理不同组件下的各个不同类型的指标,例如 http.server.request.total
Measurements
对应指标上报的具体的 DataPoint 指标数据,是一系列的数值项。
Metric Reader
用于实现对指标的数据流读取,内部定义了具体操作指标的数据结构。OpenTelemetry 官方社区提供了多种灵活的 Reader 实现,例如 PeridRader、ManualReader 等。
Metric Exporter
Exporter 用于暴露本地指标到对应的第三方厂商,例如:Promtheus、Zipkin 等。
指标类型
OpenTelemetry metrics 有许多不同指标类型,可以把它想象成类似于 int, float 这种的变量类型:
flowchart LR
subgraph 只增不减
C[Counter]
AC[Asynchronous Counter]
end
subgraph 可增可减
U[UpDownCounter]
AU[Asynchronous UpDownCounter]
G[Gauge
瞬时值]
end
subgraph 聚合统计
H[Histogram
分桶]
end**Counter:**只增不减的指标,比如 http 请求总数,字节大小;
**Asynchronous Counter:**异步 Counter;
**UpDownCounter:**可增可减的指标,比如 http 活动连接数;
**Asynchronous UpDownCounter:**异步 Counter;
**Gauge:**可增可减的指标,瞬时计量的值,比如 CPU 使用,它是异步的;
Histogram:分组聚合指标,这个较为难以理解一些,可以移步此处 查看,当然,后文也会有一个详细的例子来使用它。
实战:采集指标
废话了一堆,终于可以实战了。我们先以 http 请求总数为例来走一遍整个采集指标流程。安装扩展:
go get github.com/prometheus/client_golang
go get go.opentelemetry.io/otel/exporters/prometheus
go get go.opentelemetry.io/otel/metric
go get go.opentelemetry.io/otel/sdk/metric
打开 main.go,编写以下代码:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/otel/exporters/prometheus"
api "go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/sdk/metric"
)
const meterName = "oldme_prometheus_testing"
var (
requestHelloCounter api.Int64Counter
)
func main() {
ctx := context.Background()
// 步骤 1:创建 prometheus 导出器,数据最终从这里暴露给 Prometheus
exporter, err := prometheus.New()
if err != nil {
log.Fatal(err)
}
// 步骤 2:创建 meter provider,并挂上上面的 exporter 作为 reader
provider := metric.NewMeterProvider(metric.WithReader(exporter))
// 步骤 3:从 provider 拿一个 meter(相当于一个组件指标集)
meter := provider.Meter(meterName)
// 步骤 4:在 meter 下创建一个只增不减的 Int64 计数器
requestHelloCounter, err = meter.Int64Counter("requests_hello_total")
if err != nil {
log.Fatal(err)
}
// 步骤 5:起一个 HTTP 服务,把 /metrics 暴露出去
go serveMetrics()
ctx, _ = signal.NotifyContext(ctx, os.Interrupt)
<-ctx.Done()
}
func serveMetrics() {
log.Printf("serving metrics at localhost:2223/metrics")
http.Handle("/metrics", promhttp.Handler())
http.Handle("/index", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 记录 counter 指标:每来一个请求就 +1
requestHelloCounter.Add(r.Context(), 1)
_, _ = w.Write([]byte("Hello, Otel!"))
}))
err := http.ListenAndServe(":2223", nil) //nolint:gosec // Ignoring G114: Use of net/http serve function that has no support for setting timeouts.
if err != nil {
fmt.Printf("error serving http: %v", err)
return
}
}
在我们的代码中,我们定义一个名字为 requests_hello_total 的 Int64Counter 指标类型,Int64Counter 代表这是一个只增不减的 int64 数值,用作记录请求总数正好合适。运行我们的程序,如果不出错的话,访问 http://localhost:2223/index
可以看到 Hello, Otel!。并且我们访问 http://localhost:2223/metrics 可以看到指标数据:

这里数据还没有进行可视化,我们先把流程走通,多访问几次 http://localhost:2223/index 可以看到 requests_hello_total 会增加:

prometheus.New() 返回的 exporter 既是 Metric Reader 也是 /metrics 的 HTTP handler 数据源。很多人忘了 http.Handle("/metrics", promhttp.Handler()) 这一步,或者把 meterName 写错,最后 Prometheus 拉到的 endpoint 里根本没有 requests_hello_total。排查时先直接 curl http://localhost:2223/metrics 确认指标在,再去看 Prometheus。Histogram
接下来我们采集一下 Histogram 指标,统计在 0.1, 0.2, 0.5, 1, 2, 5 秒以内的 http 请求数,在 main.go 中加上相关代码,可以直接复制过去:
package main
import (
"context"
"fmt"
"log"
"math/rand"
"net/http"
"os"
"os/signal"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/otel/exporters/prometheus"
api "go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/sdk/metric"
)
const meterName = "oldme_prometheus_testing"
var (
requestHelloCounter api.Int64Counter
requestDurationHistogram api.Float64Histogram
)
func main() {
ctx := context.Background()
// 创建 prometheus 导出器
exporter, err := prometheus.New()
if err != nil {
log.Fatal(err)
}
// 创建 meter
provider := metric.NewMeterProvider(metric.WithReader(exporter))
meter := provider.Meter(meterName)
// 创建 counter 指标类型
requestHelloCounter, err = meter.Int64Counter("requests_hello_total")
if err != nil {
log.Fatal(err)
}
// 创建 Histogram 指标类型
requestDurationHistogram, err = meter.Float64Histogram(
"request_hello_duration_seconds",
api.WithDescription("记录 Hello 请求的耗时统计"),
api.WithExplicitBucketBoundaries(0.1, 0.2, 0.5, 1, 2, 5),
)
if err != nil {
log.Fatal(err)
}
go serveMetrics()
go goroutineMock()
ctx, _ = signal.NotifyContext(ctx, os.Interrupt)
<-ctx.Done()
}
func serveMetrics() {
log.Printf("serving metrics at localhost:2223/metrics")
http.Handle("/metrics", promhttp.Handler())
http.Handle("/index", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 记录 counter 指标
requestHelloCounter.Add(r.Context(), 1)
// 计算请求处理时间
startTime := time.Now()
// 模拟请求处理时间
time.Sleep(time.Duration(rand.Intn(3)) * time.Second)
defer func() {
duration := time.Since(startTime).Seconds()
requestDurationHistogram.Record(r.Context(), duration)
}()
_, _ = w.Write([]byte("Hello, Otel!"))
}))
err := http.ListenAndServe(":2223", nil) //nolint:gosec // Ignoring G114: Use of net/http serve function that has no support for setting timeouts.
if err != nil {
fmt.Printf("error serving http: %v", err)
return
}
}
// 随机模拟若干个协程
func goroutineMock() {
for {
go func() {
// 等待若干秒
var s = time.Duration(rand.Intn(10))
time.Sleep(s * time.Second)
}()
time.Sleep(1 * time.Millisecond)
}
}
走到这里,代码层面结束了,已经成功一半了,代码开源在 Github
。之后我们就可以安装 Prometheus 服务端和 Grafana 来进行数据可视化。
flowchart LR
A[一次请求耗时 0.37s] --> H[Histogram 分桶]
H --> B1["le=0.1: 0 个"]
H --> B2["le=0.2: 0 个"]
H --> B3["le=0.5: +1 个"]
H --> B4["le=1: +1 个"]
H --> B5["le=+Inf: +1 个"]
H --> S["_sum=0.37, _count=1"]安装 Prometheus
Prometheus 有多种安装方式,我这里依旧采用 Docker 安装,当然,你也可以使用其他方式安装,具体安装方式可以参考其他文章,后续 Grafana 同理,不在赘述,在 Promethues.yml 中填写 targets 我们的地址:
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:2223"]
Prometheus 会自动去 {{target}}/metrics 中拉取我们的指标。之后在浏览器打开 Promethues 的地址,例如我的是:http://localhost:9090,如果全部正常的话可以在 status:targets 中看见我们的指标:

在 Promethues 的首页查询 requests_hello_total 指标可以看到可视化的图表:

安装 Grafana
我的 Grafana 安装好了,登录进去后是这样的(我更改过默认颜色):

在 Data source 中添加 Prometheus 服务器,然后在 Dashboard 中添加我们想要监控的指标,即可看到更美观的图表:


sequenceDiagram
participant App as Go 应用
participant SDK as OTel SDK + Prometheus Exporter
participant Prom as Prometheus
participant Graf as Grafana
App->>SDK: requestHelloCounter.Add(1)
Note over SDK: 计数器在内存中累加
Prom->>SDK: 周期性 scrape /metrics
SDK-->>Prom: 返回 requests_hello_total 等
Graf->>Prom: 查询并渲染 Dashboard
Graf-->>App: 可视化图表 / 告警自测题与动手练习
MeterProvider、Meter、Instrument 三者是什么关系?为什么需要分这三层而不是一个 Counter 全局变量?MeterProvider 统筹全局 Meter 的创建,Meter 对应一类组件(如 HTTP 线、MySQL 线),Instrument 是某条线上的具体仪表(计数器 / 温度计)。
分三层是为了按组件隔离、统一管理生命周期,并能通过 Reader / Exporter 统一导出;若用全局 Counter 变量,无法跨组件聚合,也无法被 SDK 统一读取导出。
如果把"活动连接数"用成 Counter,连接断开时无法减回去,指标只会一路涨——监控看到的连接数永远虚高,告警和容量评估全错。
/metrics 端点抓取;你的程序只负责把指标暴露出来(如 promhttp.Handler() 挂到 /metrics),不是推给 Prometheus。默认 endpoint 叫
/metrics。prometheus.New() 在这个例子里同时扮演了哪两个角色?MeterProvider 上负责周期性读数),又是 /metrics 的 HTTP handler 数据源(通过 promhttp.Handler() 暴露)。忘了挂 /metrics 或 MeterProvider 没接它,Prometheus 就拉不到数据。动手练习(建议真做一遍):
- 把文中的 Counter 示例跑起来,用
curl http://localhost:2223/metrics | grep requests_hello_total确认指标随访问次数增长。 - 在 Histogram 示例基础上,把桶边界改成
0.05, 0.1, 0.25, 0.5, 1,观察/metrics里_bucket行数变化,理解分桶粒度对分位统计的影响。 - 用 Docker 起 Prometheus + Grafana,把
requests_hello_total和request_hello_duration_seconds_count各画一张 Panel,体会从"原始指标"到"可看图表"的最后一公里。
本章小结
- 指标抽象是四层链:
MeterProvider(工厂)→Meter(组件)→Instrument(具体仪表)→Measurements(数据点),Reader/Exporter负责读取并导出。 - 指标类型要按语义选:只增不减用 Counter,可增可减用 UpDownCounter,瞬时值用 Gauge,要算分位用 Histogram。
- Go 最小链路是:建
prometheusexporter → 挂到MeterProvider→ 取Meter→ 建Instrument→ 在 handler 里Add/Record→promhttp.Handler()暴露/metrics。 - Histogram 靠"显式桶边界"分桶,才能算出 P99 等长尾指标,避免被平均值掩盖。
- 数据流向是"应用埋点 → Prometheus 拉取
/metrics→ Grafana 查询可视化",Prometheus 是主动拉模型。
下一篇可以深入 OTel 三大支柱(Metrics / Traces / Logs)的体系化用法,以及 Collector 的进阶配置。