基于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语言编写,部署开销相较其他方案而言较为轻量,适宜在中小型团队内使用。官方目前仍在活跃更新中,建议大家如果采用此方案及时跟进官方的更新内容。

相关推荐
chenzhuyu几秒前
海康摄像机在Edge浏览器的网页无法直接预览,按照要求安装WebComponents后仍然提示请安装插件或预览失败的解决办法
运维
极小狐4 分钟前
如何减少极狐GitLab 容器镜像库存储?
运维·git·rpc·kubernetes·ssh·gitlab·terraform
cooldream200916 分钟前
深入理解负载均衡:传输层与应用层的原理与实战
运维·负载均衡·系统架构师
YoungHong199221 分钟前
Ubuntu通过源码编译方式单独安装python3.12
linux·运维·python·ubuntu
IT小饕餮37 分钟前
华为设备MSTP
运维·华为
传知摩尔狮42 分钟前
深入理解 Linux 虚拟文件系统(VFS)
linux·运维·服务器·性能优化
大G哥1 小时前
云计算-私有云-私有云服务运维
运维·云计算
不爱吃萝卜的嘤嘤怪1 小时前
如何删除vscod远程连接的服务器
运维·服务器·策略模式
2401_858286111 小时前
OSE2.【Linux】练习:查找项目的main函数入口
linux·运维·服务器
舰长1151 小时前
docker 镜像的导出和导入(导出完整镜像和导出容器快照)
linux·运维·服务器