探索 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 这种的变量类型:
类比:指标类型就像不同的"计量工具"。Counter 是只能往上加的里程表(不能倒退);UpDownCounter 是能上能下的库存计数器;Gauge 是温度计,反映"此刻"的瞬时值;Histogram 是带刻度分级的量杯,不但记总数,还帮你按区间分桶统计。
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 来进行数据可视化。
为什么需要 Histogram 而不是只记总耗时? 单一的平均值会掩盖长尾。Histogram 把耗时按桶边界(0.1 / 0.2 / 0.5 / 1 / 2 / 5 秒)分桶,Prometheus 才能算出 P50 / P95 / P99 这类分位数,帮你发现"大多数请求很快、但少数请求极慢"的长尾问题。
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 同理,不在赘述,在 Prometheus.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全局变量?- Counter 和 UpDownCounter 在语义上最核心的区别是什么?举一个"用错类型会出 bug"的例子。
- Histogram 的"显式桶边界"有什么用?为什么只看平均耗时不够?
- Prometheus 是怎么拿到你的指标的——是你的程序"推"给它的,还是它来"拉"你的?拉取的 endpoint 默认叫什么?
prometheus.New()在这个例子里同时扮演了哪两个角色?
动手练习(建议真做一遍):
- 把文中的 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 的进阶配置。