Go 监控告警入门 Opentelemetry

2024-06-03T14:11:02+08:00 | 9分钟阅读 | 更新于 2024-06-03T14:11:02+08:00

@

探索 Go 语言中 Opentelemetry 与 Prometheus 集成,导出 HTTP 服务指标监控,并最终将 Prometheus 指标可视化到 Grafana 中。

学习目标

学完本章你应该能够:

  1. 讲清 OpenTelemetry Metrics 的核心抽象链:MeterProviderMeterInstrumentMeasurements,以及 Metric Reader / Metric Exporter 在其中的角色。
  2. 区分 Counter / UpDownCounter / Gauge / Histogram 四种指标类型的语义,知道"只增不减"和"瞬时值"分别该用哪个。
  3. 用 Go 代码跑通"创建一个 Int64Counter,在 HTTP handler 里 Add(1),再通过 /metrics 暴露"的最小链路。
  4. 理解 Histogram 用"显式桶边界"做分位统计的原理,能给 request_hello_duration_seconds 自定义桶。
  5. 把数据接入 Prometheus(拉取 /metrics)和 Grafana(数据源 + Dashboard)做可视化,讲清"应用 → Exporter → Prometheus → Grafana"的流向。

前置知识

  • Go 语言基础(package main、HTTP handler、context
  • 一点 Prometheus 概念(它会主动"拉"你的 /metrics 端点)
  • Docker 基础(文末用 Docker 起 Prometheus / Grafana)

本章你会动手做的事

  1. go get 三个依赖,把"请求计数"的 Counter 跑起来,访问 /indexrequests_hello_total 增长。
  2. 加一个 Histogram,用 rand 模拟耗时,观察 /metrics_bucket / _sum / _count 三件套。
  3. 用 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

image-20250425022002941

基础概念

这里为了简单入门,尽量简单的介绍一些抽象概念,结合着代码理解,如果不能理解也没关系,代码写着写着自然就明白了:

类比:把指标系统想成一家工厂。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_totalInt64Counter 指标类型,Int64Counter 代表这是一个只增不减的 int64 数值,用作记录请求总数正好合适。运行我们的程序,如果不出错的话,访问 http://localhost:2223/index 可以看到 Hello, Otel!。并且我们访问 http://localhost:2223/metrics 可以看到指标数据:

image-20250425022039766

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

image-20250425022056607

⚠️ 新手必踩的坑: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 中看见我们的指标:

image-20250425022121700

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

image-20250425022142429

安装 Grafana

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

image-20250425022158729

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

image-20250425022219007

image-20250425022234942

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: 可视化图表 / 告警

自测题与动手练习

自测题(合上书能答出来,才算懂)

  1. MeterProviderMeterInstrument 三者是什么关系?为什么需要分这三层而不是一个 Counter 全局变量?
  2. Counter 和 UpDownCounter 在语义上最核心的区别是什么?举一个"用错类型会出 bug"的例子。
  3. Histogram 的"显式桶边界"有什么用?为什么只看平均耗时不够?
  4. Prometheus 是怎么拿到你的指标的——是你的程序"推"给它的,还是它来"拉"你的?拉取的 endpoint 默认叫什么?
  5. prometheus.New() 在这个例子里同时扮演了哪两个角色?

动手练习(建议真做一遍)

  1. 把文中的 Counter 示例跑起来,用 curl http://localhost:2223/metrics | grep requests_hello_total 确认指标随访问次数增长。
  2. 在 Histogram 示例基础上,把桶边界改成 0.05, 0.1, 0.25, 0.5, 1,观察 /metrics_bucket 行数变化,理解分桶粒度对分位统计的影响。
  3. 用 Docker 起 Prometheus + Grafana,把 requests_hello_totalrequest_hello_duration_seconds_count 各画一张 Panel,体会从"原始指标"到"可看图表"的最后一公里。

本章小结

  • 指标抽象是四层链:MeterProvider(工厂)→ Meter(组件)→ Instrument(具体仪表)→ Measurements(数据点),Reader/Exporter 负责读取并导出。
  • 指标类型要按语义选:只增不减用 Counter,可增可减用 UpDownCounter,瞬时值用 Gauge,要算分位用 Histogram。
  • Go 最小链路是:建 prometheus exporter → 挂到 MeterProvider → 取 Meter → 建 Instrument → 在 handler 里 Add/Recordpromhttp.Handler() 暴露 /metrics
  • Histogram 靠"显式桶边界"分桶,才能算出 P99 等长尾指标,避免被平均值掩盖。
  • 数据流向是"应用埋点 → Prometheus 拉取 /metrics → Grafana 查询可视化",Prometheus 是主动拉模型。

下一篇可以深入 OTel 三大支柱(Metrics / Traces / Logs)的体系化用法,以及 Collector 的进阶配置。

About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!