作者参与的一个项目中针对系统监控选择了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)
}
可供参考的其他中间件接入方案:
- redis redis.uptrace.dev/guide/go-re...
- kafka github.com/twmb/franz-...
- net/http github.com/open-teleme...
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语言编写,部署开销相较其他方案而言较为轻量,适宜在中小型团队内使用。官方目前仍在活跃更新中,建议大家如果采用此方案及时跟进官方的更新内容。