基于OpenObserve实现分布式系统监控(1)

作者参与的一个项目中针对系统监控选择了Openobserve这个比较新的系统搭建,实现了日志+指标+链路的完整监控体系。本文以此项目作为例子,分享一下我们团队针对Openobserve平台接入的一些实现方案和经验。

Openobserve部署

如果想要在单机部署Openobserve,官方文档中提供了两种方法,分别是通过安装脚本直接在裸机安装、及通过docker run命令在docker中启动。在实际部署中,我们发现这两种方案均不太合适。其中第一种方案会污染物理机,而本人团队的监控机器上需要同时部署其他应用,有发生冲突的危险。第二种方案不利于后期变更docker容器相关配置,且教程中使用的是社区版本镜像,其功能受限。

鉴于以上原因,我们团队选择了使用docker compose的方案,部署企业版本的Openobserve系统,以下为示例配置文件:

bash 复制代码
services:
  openobserve:
    image: public.ecr.aws/zinclabs/openobserve-enterprise:latest
    ports:
      - "5080:5080" # web
      - "5081:5081" # grpc
      - "5514:5514" # syslog
    environment:
      - ZO_DATA_DIR=/data
      - ZO_ROOT_USER_EMAIL=******@example.com
      - ZO_ROOT_USER_PASSWORD=******
      - ZO_TELEMETRY=false  # 禁用Openobserve遥测
      - ZO_APP_NAME=observe
      - ZO_COMPACT_DATA_RETENTION_DAYS=14 # 日志滚动周期
    volumes:
      - openobserve_data:/data
    restart: unless-stopped
volumes:
  openobserve_data: 

针对企业版付费问题,官方团队曾在团队博客中给出过澄清,大致内容为:企业版可以对日均日志吞吐低于200GB的团队免费使用,这对于中小团队而言是绰绰有余的额度,可以放心使用。

成功部署后登录即可看到如下界面,证明部署部分已经成功完成:

增强配置

如果想在以上基础上额外补充单点登录功能,Openobserve依赖dexidp实现对接外部身份源,而此依赖的部署方案官方仅在helm镜像包中有提及,但并未提供docker compose版本的部署方案。经过一段时间摸索,我们团队最终将helm包中的部署有关配置移植到了compose中,具体配置如下:

bash 复制代码
# docker-compose.yaml
services:
  openobserve:
    image: public.ecr.aws/zinclabs/openobserve-enterprise:latest
    ports:
      - "5080:5080"
      - "5081:5081" # grpc 
      - "5514:5514" # syslog
      # 其他端口可按需暴露,或直接使用host网络模式
    environment:
      - ZO_DATA_DIR=/data
      - ZO_ROOT_USER_EMAIL=*****@example.com
      - ZO_ROOT_USER_PASSWORD=*******
      - ZO_BASE_URI=/observe  # 网站路径前缀
      - ZO_TELEMETRY=false
      - ZO_APP_NAME=observe
      - ZO_COMPACT_DATA_RETENTION_DAYS=30
      - O2_DEX_ENABLED=true
      - O2_DEX_CLIENT_ID=o2-client
      - O2_DEX_CLIENT_SECRET=******
      - O2_DEX_BASE_URL=https://***.com/dex
      - O2_DEX_REDIRECT_URL=https://***.com/observe/config/redirect
      - O2_CALLBACK_URL=https://***.com/observe/web/cb
    volumes:
      - openobserve_data:/data
    restart: unless-stopped
  dex:
    image: ghcr.io/dexidp/dex:v2.37.0
    ports:
      - "5556:5556" # dex endpoint
    volumes:
      - ./dex-config.yaml:/etc/dex/config.docker.yaml
volumes:
  openobserve_data:
yaml 复制代码
# dex-config.yaml
issuer: https://example.com/internal/dex

storage:
  type: memory  # 分布式环境下不应该使用内存数据库,此处仅做示意

web:
  http: 0.0.0.0:5556

expiry:
  idTokens: 10m
  refreshTokens:
    validIfNotUsedFor: 30m

staticClients:
  - id: o2-client
    redirectURIs:
      - https://example.com/internal/observe/config/redirect
    name: o2-client
    secret: ****** # 与上文O2_DEX_CLIENT_SECRET对应

oauth2:
  responseTypes:
    - code
  skipApprovalScreen: true

connectors:
  - type: oidc  # OIDC认证协议介入,可以根据上游身份源自行选择
    id: sso
    name: SSO
    config:
      issuer: https://sso.***.com/application/o/observe/
      clientID: xxxx
      clientSecret: xxxxx
      redirectURI: https://***.com/dex/callback

注意:需要在外层通过Nginx、Caddy等将对应端口暴露为配置中的域名

成功部署后应看到如下登录页面,点击Login with SSO后即可跳转到身份源完成单点登录:

链路监控

Go系统接入

团队内使用的微服务以fiber作为web服务框架,基于官方的otelfiber插件二次开发,实现链路数据上报。

连接Openobserve系统方式:

go 复制代码
const (
    DefaultTracesPath  = "/v1/traces"
    DefaultMetricsPath = "/v1/metrics"
    DefaultLogsPath    = "/v1/logs"
)

func ConfigureOpentelemetry(ctx context.Context, serviceName string) func() {
    endpoint := os.Getenv("OTEL_HTTP_ENDPOINT")
    authorization := os.Getenv("OTEL_AUTHORIZATION")

    if endpoint == "" || authorization == "" {
       return configureStdout()
    }

    endpointUrl, err := url.Parse(endpoint)
    if err != nil {
       panic(err)
    }

    hostname, err := os.Hostname()
    if err != nil {
       hostname = "unknown"
    }
    res := resource.NewWithAttributes(
       semconv.SchemaURL,
       semconv.ServiceNameKey.String(serviceName),
       semconv.HostNameKey.String(hostname),
    )

    isInsecure := strings.ToLower(endpointUrl.Scheme) == "http"

    authHeader := map[string]string{
       "Authorization": authorization,
       "stream-name":   "default",
    }

    // Tracing
    traceOptions := []otlptracehttp.Option{
       otlptracehttp.WithEndpoint(endpointUrl.Host),
       otlptracehttp.WithURLPath(path.Join(endpointUrl.Path, DefaultTracesPath)),
       otlptracehttp.WithHeaders(authHeader),
    }
    if isInsecure {
       traceOptions = append(traceOptions, otlptracehttp.WithInsecure())
    }

    traceExp, err := otlptracehttp.New(ctx, traceOptions...)

    if err != nil {
       panic(err)
    }

    tracerProvider := sdktrace.NewTracerProvider(
       sdktrace.WithSampler(sdktrace.AlwaysSample()),
       sdktrace.WithResource(res),
       sdktrace.WithBatcher(traceExp),
    )

    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
       propagation.TraceContext{},
       propagation.Baggage{},
    ))
    otel.SetTracerProvider(tracerProvider)

    return func() {
       cxt, cancel := context.WithTimeout(ctx, time.Second)
       defer cancel()
       if err := traceExp.Shutdown(cxt); err != nil {
          otel.Handle(err)
       }
    }
}

由于Golang基于context上下文实现并发控制,为了实现链路收集,需要在所有链接函数中持续传递context对象,并使用opentelemetry为不同中间件驱动实现的instrumentation组件实现上报。对于微服务系统,opentelemetry目前通过请求头设置Traceparent实现链路传播(propagation),其内容格式为version-traceId-parentId-traceFlags,详见W3C规范文档

一个典型的MySQL接入方案是基于otelsql,实现针对database/sql包的链路数据上报。具体实现如下:

go 复制代码
db, err := otelsql.Open(
   dialect.MySQL,
   fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=True", *****),
   otelsql.WithAttributes(semconv.DBSystemMySQL),
   otelsql.WithSpanOptions(otelsql.SpanOptions{
      DisableErrSkip: true,
   }),
   otelsql.WithDisableSkipErrMeasurement(true),
)
if err != nil {
   panic(err)
}
err = otelsql.RegisterDBStatsMetrics(db, otelsql.WithAttributes(
   semconv.DBSystemMySQL,
))
if err != nil {
   panic(err)
}

可供参考的其他中间件接入方案:

Python系统接入

Python接入与Golang基本思路相似,具体如下:

python 复制代码
resource = Resource(attributes={
    "service.name": "ezcoding-langchain",
    "host.name": socket.gethostname(),
})
tracer_provider = TracerProvider(resource=resource)
trace.set_tracer_provider(tracer_provider)

otlp_endpoint = os.getenv("OTEL_HTTP_ENDPOINT")
authorization = os.getenv("OTEL_AUTHORIZATION")

otlp_exporter = OTLPSpanExporter(
    endpoint=otlp_endpoint + "/v1/traces",
    headers={
        "Authorization": authorization,
        "stream-name": "default",
    }
)
tracer_provider.add_span_processor(
    BatchSpanProcessor(otlp_exporter)
)

FastAPI框架接入方式:

python 复制代码
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
FastAPIInstrumentor.instrument_app(app, exclude_spans=["send"])

OpenAI客户端接入方式:

python 复制代码
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
OpenAIInstrumentor().instrument()

LangChain框架可尝试使用此项目:github.com/traceloop/o...

Django框架官方目前支持仍不完善,对于WSGI方式启动的项目,可以直接使用:opentelemetry官方实现的instrumentation。对于ASGI项目,该实现目前无法收集和上报数据库相关链路数据,且上报的请求信息中对于路由地址的解析有误,具体解决还有待官方修复。针对这个问题目前团队内选择使用opentelemetry-instrumentation-mysqlclient库手动上报数据库链路,并通过monkey-patch的方式临时解决Django路由解析问题。

实现效果

如图所见,通过OpenObserve系统,我们实现了链路数据上报及可视化。

后记

根据笔者团队这段时间的使用,发现OpenObserve存在日志压缩机制,因此建议准备接入该平台的团队搭配告警功能使用,在获得故障链路后及时通知运维开发同事。由于该项目使用Rust语言编写,部署开销相较其他方案而言较为轻量,适宜在中小型团队内使用。官方目前仍在活跃更新中,建议大家如果采用此方案及时跟进官方的更新内容。

相关推荐
孤寂大仙v1 分钟前
【Linux笔记】理解文件系统(上)
linux·运维·笔记
沉默的八哥20 分钟前
K8S高可用Web应用部署方案
运维
winyh530 分钟前
Vite 打包后Nginx部署配置
运维·nginx
运维小贺2 小时前
Nginx常用的模块
运维·nginx·正则表达式
努力学习的小廉2 小时前
深入了解Linux —— 调试程序
linux·运维·服务器
努力学习的小廉2 小时前
深入了解Linux —— git三板斧
linux·运维·git
AI学IT2 小时前
(安全防御)旁挂组网双机热备负载分担实验
运维·服务器·网络
code monkey.3 小时前
【寻找Linux的奥秘】第一章:基础指令
linux·运维·服务器
qziovv3 小时前
Ubuntu通过局域网共享文件夹实现文件夹的连接
linux·运维·ubuntu
rkmhr_sef4 小时前
Nginx反向代理出现502 Bad Gateway问题的解决方案
运维·nginx·gateway