Skip to content

44.OpenObserve

OpenObserve 是一个 Rust 开发的开源的高性能云原生可观测平台(日志、指标、追踪),比起 Elasticsearch 它大约可以节省 140 倍的存储成本,OpenObserve 能够处理 PB 级的数据,如果你正在寻找一个用于日志、指标、追踪的可观测工具,那么 OpenObserve 是非常值得尝试的。

OpenObserve 目前处于 alpha 阶段,但其实也进行了广泛的测试,下表是目前的可用功能和路线图。

# Feature Status
1 日志搜索 Available
2 高压缩比的数据存储 Available
3 动态演化模式 Available
4 开箱即用的身份验证 Available
5 支持使用 S3、MinIO、GCS、Azure blob 进行数据存储 Available
6 高级图像界面 Available
7 基于 SQL 的查询语言 Available
8 支持高基数的数据 Available
9 搜索日志周围数据 Available
10 用户自定义摄取和查询函数(基于 VRL) Available
11 多组户 Available
12 兼容 Elasticsearch 的摄取 API Available
13 兼容 Elasticsearch 的搜索和聚合 API Through zPlane
14 计划报警 (Based on logs) Available
15 实时报警 (Based on logs) Available
16 高可用和集群 Available
17 无状态节点 Available
18 多语言支持 Available
19 多平台支持的预构建的二进制文件 Available
20 多平台支持的预构建的容器镜像 Available
21 带 SIMD 加速的预构建容器镜像 Available
22 SIMD 支持向量化处理 (AVX512 and Neon) Available
23 Dashboards Available
24 Metrics Available
25 支持 metrics 的 PromQL Available (部分支持 - 进行中)
26 Traces Available
27 计划报警 (Based on metrics) Available
28 实时报警 (Based on metrics) Available
29 基于模板的报警目标 (允许发生到 slack、teams 等) Available
30 发生报警到 Alertmanager Available
31 使用 Kinesis firehose 摄取 AWS 日志 (cloudwatch、VPC 流量日志、AWS WAF 等) Available
32 单点登录(SSO) 正在启动...

OpenObserve 与 Elasticsearch 的比较

Elasticsearch 是一个通用搜索引擎,可以使用应用程序搜索或日志搜索。OpenObserve 是专门为日志搜索而构建的,如果你正在寻找 Elasticsearch 的轻量级替代品,那么您应该看看 ZincSearch,如果只是想要一个日志搜索引擎,那么 OpenObserve 是一个非常好的选择。

OpenObserve 不依赖于数据索引,它将未索引的数据以压缩格式存储在本地磁盘或以 parquet 列格式的对象存储中。这使得数据摄取期间的计算要求大大降低,并且压缩率非常高,从而使存储成本降低约 140 倍。没有数据索引意味着全扫描搜索可能比 Elasticsearch 慢,但由于分区和缓存等多种其他技术,仍然应该很快。Uber 发现其生产环境中 80% 的查询是聚合查询,而 OpenObserve 的列式数据存储意味着聚合查询通常比 Elasticsearch 快得多。

下面是我们使用 Fluentbit 将真实日志数据从 Kubernetes 集群发送到 Elasticsearch 和 OpenObserve 时的结果,这只与存储有关。EBS 卷的成本为 8 美分/GB/月 (GP3),s3 的成本为 2.3 美分/GB/月。在 Elasticsearch 的 HA 模式下,通常有 1 个主节点和 2 个副本。无需复制 s3 来实现数据持久性/可用性,因为 AWS 会将你的对象冗余存储在 Amazon S3 区域中至少三个可用区 (AZ) 的多个设备上。

img

在上述场景中,OpenObserve 具有比 Elasticsearch 低 140 倍的存储成本的显着优势,这甚至没有考虑额外未使用的 EBS 卷容量(为了不耗尽磁盘空间而需要提供这些容量)以及持续监控磁盘使用情况以使其不被填满所需的工作。

无状态节点架构允许 OpenObserve 水平扩展,而无需担心数据复制或损坏。与 Elasticsearch 相比,您通常会发现管理 OpenObserve 集群的运维工作量和成本要低得多。

OpenObserve 内置的图形用户界面消除了对 Kibana 等其他组件的需求,而且由于 Rust 的优势,性能出色,而无需面对 JVM 所带来的问题。

与 Elasticsearch 相比,Elasticsearch 是一个通用性的搜索引擎,同时也兼具观测工具的功能。而 OpenObserve 是从头开始构建的观测工具,非常注重提供优秀的可观测性能。

架构

OpenObserve 可以在单节点下运行,也可以在集群中以 HA 模式运行。

单节点模式

单节点模式也分几种架构,主要是数据存储的方式不同,主要有如下几种:

Sled 和本地磁盘模式

如果你只需要进行简单使用和测试,或者对高可用性没有要求,可以使用此模式。当然你仍然可以在一台机器上每天处理超过 2 TB 的数据。在我们的测试中,使用默认配置,Mac M2 的处理速度为约 31 MB/秒,即每分钟处理 1.8 GB,每天处理 2.6 TB。该模式也是运行 OpenObserve 的默认模式。

img

Sled 和对象存储模式

该模式和 OpenObserve 的默认模式基本上一致,只是数据存在了对象存储中,这样可以更好的支持高可用性,因为数据不会丢失。

img

Etcd 和对象存储模式

该模式是使用 Etcd 来存储元数据,数据仍然存储在对象存储中。

img

HA 模式

HA 模式不支持本地磁盘存储,集群模式下 OpenObserve 会运行多个节点,每个节点都是无状态的,数据存储在对象存储中,元数据存储在 Etcd 中,这样可以更好的支持高可用性,因为数据不会丢失。

img

在该模式下 OpenObserve 主要包括 Router、Querier、Ingester 和 Compactor 四个组件,这些组件都可以水平扩展;Etcd 用于存储用户、函数、报警规则和集群节点信息等元数据;对象存储(例如 s3、minio、gcs 等等)存储 parquet 文件和文件列表索引的所有数据。

Router

Router 路由器将请求分发给 ingester 或 querier,它还通过浏览器提供 UI 界面。Router 实际上就是一个非常简单的代理,用于在数据摄入程序和查询程序之间发送适当的请求并进行响应。

Ingester

Ingester 用于接收摄取请求并将数据转换为 parquet 格式然后存储在对象存储中,它们在将数据传输到对象存储之前将数据临时存储在 WAL 中。

Querier

Querier 用于查询数据,查询器节点是完全无状态的。

Compactor

Compactor 会将小文件合并成大文件,使搜索更加高效。 Compactor 还处理数据保留策略、full stream 删除和文件列表索引更新。

AlertManager

Alertmanager 报警模块。

安装

OpenObserve 的安装非常简单,只需要下载二进制文件即可,它支持 Linux、Windows 和 MacOS,也支持 Docker 镜像。我们这里当然还是将其安装到 Kubernetes 集群中,为简单这里我们直接使用默认的 Sled 和本地磁盘模式。

首先创建一个命名空间:

$ kubectl create ns openobserve

然后创建如下所示的资源清单文件:

# openobserve.yaml
apiVersion: v1
kind: Service
metadata:
  name: openobserve
  namespace: openobserve
spec:
  clusterIP: None
  selector:
    app: openobserve
  ports:
    - name: http
      port: 5080
      targetPort: 5080
---
# create statefulset
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: openobserve
  namespace: openobserve
  labels:
    app: openobserve
spec:
  serviceName: openobserve
  replicas: 1
  selector:
    matchLabels:
      app: openobserve
  template:
    metadata:
      labels:
        app: openobserve
    spec:
      securityContext:
        fsGroup: 2000
        runAsUser: 10000
        runAsGroup: 3000
        runAsNonRoot: true
      containers:
        - name: openobserve
          image: public.ecr.aws/zinclabs/openobserve:latest
          env:
            - name: ZO_ROOT_USER_EMAIL # 指定管理员邮箱
              value: root@example.com
            - name: ZO_ROOT_USER_PASSWORD # 指定管理员密码
              value: root321
            - name: ZO_DATA_DIR
              value: /data
          imagePullPolicy: Always
          resources:
            limits:
              cpu: 4096m
              memory: 2048Mi
            requests:
              cpu: 256m
              memory: 50Mi
          ports:
            - containerPort: 5080
              name: http
          volumeMounts:
            - name: data
              mountPath: /data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: cfsauto # 指定一个可用的存储类
        resources:
          requests:
            storage: 10Gi

上面的资源清单中,我们使用了一个 StatefulSet 来创建 OpenObserve,需要注意的是需要配置 ZO_ROOT_USER_EMAILZO_ROOT_USER_PASSWORD 两个环境变量用来指定管理员邮箱和密码。然后在 PVC 模板中指定一个可用的 StorageClass,用于持久化存储数据。

然后直接应用上面的资源清单文件即可:

$ kubectl apply -f openobserve.yaml
$ kubectl get pods -n openobserve
NAME            READY   STATUS    RESTARTS   AGE
openobserve-0   1/1     Running   0          2m31s
$ kubectl get svc -n openobserve
NAME          TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
openobserve   ClusterIP   None         <none>        5080/TCP   2m52s

快速使用

创建后我们可以查看一下 OpenObserve 的日志来验证是否启动成功:

$ kubectl logs -f openobserve-0 -n openobserve
[2023-08-04T10:18:06Z INFO  openobserve] Starting OpenObserve v0.5.1
[2023-08-04T10:18:06Z INFO  openobserve::service::db::user] get; org_id=Some("default") name="root@example.com"
[2023-08-04T10:18:06Z INFO  tracing::span] set;
[2023-08-04T10:18:06Z INFO  openobserve::service::db::user] Users Cached
# ......
[2023-08-04T10:18:06Z INFO  openobserve::common::meta::telemetry] sending event OpenObserve - Starting server
[2023-08-04T10:18:07Z INFO  actix_server::builder] starting 4 workers
[2023-08-04T10:18:07Z INFO  actix_server::server] Tokio runtime found; starting in existing Tokio runtime
[2023-08-04T10:18:07Z INFO  openobserve] starting HTTP server at: 0.0.0.0:5080, thread_id: 0
[2023-08-04T10:18:07Z INFO  openobserve] starting HTTP server at: 0.0.0.0:5080, thread_id: 0
[2023-08-04T10:18:07Z INFO  openobserve] starting HTTP server at: 0.0.0.0:5080, thread_id: 0

启动后我们可以通过 kubectl port-forward 命令将 OpenObserve 的 5080 端口映射到本地,然后在浏览器中访问 http://localhost:5080 即可看到 OpenObserve 的 UI 界面。

$ kubectl port-forward svc/openobserve 5080:5080 -n openobserve
Forwarding from 127.0.0.1:5080 -> 5080
Forwarding from [::1]:5080 -> 5080

img

使用上面指定的管理员邮箱和密码即可登录,然后就可以看到 OpenObserve 的主界面:

img

因为现在还没有数据,所以页面中没有任何内容,在 ingestion 页面提供了 Logs、Metrics、Traces 数据的各种摄取方法:

img

这里我们可以先使用 JSON API 来加载一些示例日志数据来了解一下 OpenObserve 的使用方法。先使用下面命令下载示例日志数据:

$ curl -L https://zinc-public-data.s3.us-west-2.amazonaws.com/zinc-enl/sample-k8s-logs/k8slog_json.json.zip -o k8slog_json.json.zip
$ unzip k8slog_json.json.zip

然后使用下面命令将示例日志数据导入到 OpenObserve 中:

$ curl http://localhost:5080/api/default/default/_json -i -u "root@example.com:root321"  -d "@k8slog_json.json"
HTTP/1.1 100 Continue

HTTP/1.1 200 OK
content-length: 71
vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers
content-type: application/json
date: Fri, 04 Aug 2023 10:46:46 GMT

{"code":200,"status":[{"name":"default","successful":3846,"failed":0}]}%

收据导入成功后,刷新页面即可看到有数据了:

img

在 Stream 页面可以看到我们导入的数据元信息:

img

然后可以切换到 Logs 页面就可以看到日志数据了:

img

现在我们就可以去根据直接的需求去查询日志了,常用的一些查询语法如所示:

  • 对于值 error 的全文搜索,在查询编辑器中使用 match_all('error')
  • 对于值 error 的不区分大小写的全文搜索,使用 match_all_ignore_case('error')
  • 对于值 error 的列搜索,使用 str_match(fieldname, 'error'),这比 match_all 更有效,因为它在单个字段中搜索。
  • 要搜索 code 列的值 200,使用 code=200
  • 要搜索列 stream 列的值为 stderr,使用stream='stderr'
  • 要在日志 log 列上搜索和使用查询函数 extract_ip,使用 extract_ip(log) | code=200

部署 Fluentd

上面我们通过手动导入示例日志数据来了解 OpenObserve 的基本使用,在生产环境中我们通常会使用日志采集器来收集日志数据,这里我们就来使用 Fluentd 来收集日志数据。

完整的资源清单文件如下所示:

# fluentd.yaml
kind: ConfigMap
apiVersion: v1
metadata:
  name: fluentd-conf
  namespace: kube-log
data:
  # 容器日志
  containers.input.conf: |-
    <source>
      @id fluentd-containers.log
      @type tail                              # Fluentd 内置的输入方式,其原理是不停地从源文件中获取新的日志
      path /var/log/containers/*.log          # 容器日志路径
      pos_file /var/log/es-containers.log.pos  # 记录读取的位置
      tag raw.kubernetes.*                    # 设置日志标签
      read_from_head true                     # 从头读取
      <parse>                                 # 多行格式化成JSON
        # 可以使用我们介绍过的 multiline 插件实现多行日志
        @type multi_format                    # 使用 multi-format-parser 解析器插件
        <pattern>
          format json                         # JSON解析器
          time_key time                       # 指定事件时间的时间字段
          time_format %Y-%m-%dT%H:%M:%S.%NZ   # 时间格式
        </pattern>
        <pattern>
          format /^(?<time>.+) (?<stream>stdout|stderr) [^ ]* (?<log>.*)$/
          time_format %Y-%m-%dT%H:%M:%S.%N%:z
        </pattern>
      </parse>
    </source>

    # 在日志输出中检测异常(多行日志),并将其作为一条日志转发
    # https://github.com/GoogleCloudPlatform/fluent-plugin-detect-exceptions
    <match raw.kubernetes.**>           # 匹配 tag 为 raw.kubernetes.** 日志信息
      @id raw.kubernetes
      @type detect_exceptions           # 使用 detect-exceptions 插件处理异常栈信息
      remove_tag_prefix raw             # 移除 raw 前缀
      message log
      multiline_flush_interval 5
    </match>

    <filter **>  # 拼接日志
      @id filter_concat
      @type concat                # Fluentd Filter 插件,用于连接多个日志中分隔的多行日志
      key message
      multiline_end_regexp /\n$/  # 以换行符 \n 拼接
      separator ""
    </filter>

    # 添加 Kubernetes metadata 数据
    <filter kubernetes.**>
      @id filter_kubernetes_metadata
      @type kubernetes_metadata
      annotation_match ["^k8slog"]  # 保留匹配以 `k8slog` 的注解日志
      skip_labels true
      skip_container_metadata true
      skip_namespace_metadata true
      skip_master_url true
    </filter>

    # 只保留具有 `k8slog: true` 注解的Pod日志
    <filter kubernetes.**>
      @id filter_log
      @type grep
      <regexp>
        key $['kubernetes']['annotations']['k8slog']
        pattern true
      </regexp>
    </filter>

    # 删除一些多余的属性
    <filter kubernetes.**>
      @type record_transformer
      remove_keys $.docker.container_id,$.kubernetes.pod_id,$.kubernetes.annotations.k8slog
    </filter>

  output.conf: |-
    <match **>
      @type http # 使用http插件
      endpoint http://openobserve.openobserve.svc.cluster.local:5080/api/default/K8sLogs/_json # 指定接收日志的地址
      content_type json
      json_array true
      <auth>
        method basic
        username root@example.com
        password root321
      </auth>
      <buffer>
        flush_interval 2s   # 每2秒发送一次
      </buffer>
    </match>

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluentd
  namespace: kube-log
  labels:
    k8s-app: fluentd
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: fluentd
  labels:
    k8s-app: fluentd
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
rules:
  - apiGroups:
      - ""
    resources:
      - "namespaces"
      - "pods"
    verbs:
      - "get"
      - "watch"
      - "list"
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: fluentd
  labels:
    k8s-app: fluentd
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
subjects:
  - kind: ServiceAccount
    name: fluentd
    namespace: kube-log
    apiGroup: ""
roleRef:
  kind: ClusterRole
  name: fluentd
  apiGroup: ""
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: kube-log
  labels:
    app: fluentd
    kubernetes.io/cluster-service: "true"
spec:
  selector:
    matchLabels:
      app: fluentd
  template:
    metadata:
      labels:
        app: fluentd
        kubernetes.io/cluster-service: "true"
    spec:
      tolerations:
        - key: node-role.kubernetes.io/control-plane
          effect: NoSchedule
      serviceAccountName: fluentd
      containers:
        - name: fluentd
          image: quay.io/fluentd_elasticsearch/fluentd:v4.4.3
          volumeMounts:
            - name: fluentconfig
              mountPath: /etc/fluent/config.d
            - name: varlog
              mountPath: /var/log
      volumes:
        - name: fluentconfig
          configMap:
            name: fluentd-conf
        - name: varlog
          hostPath:
            path: /var/log

上面的资源清单文件中,我们使用了 Fluentd 的 tail 插件来收集容器日志,通过 kubernetes_metadata 插件添加 Kubernetes 的元数据,然后使用 grep 插件来过滤出具有 k8slog: true 注解的 Pod 日志,最后使用 http 插件将日志数据发送到 OpenObserve 中,其中 endpoint 参数指定了 OpenObserve 的 API 地址,格式为 http://localhost:5080/api/{organization}/{stream}/_jsonusernamepassword 参数指定了管理员的邮箱和密码。

直接应用上面的资源清单文件即可:

$ kubectl apply -f fluentd.yaml
$ kubectl get pods -n kube-log
NAME                       READY   STATUS    RESTARTS   AGE
fluentd-46brr              1/1     Running   0          22m
fluentd-7wbqv              1/1     Running   0          22m
fluentd-qblqz              1/1     Running   0          22m

然后创建一个带有 k8slog: "true" 的注解的 Pod 来测试一下:

# counter.yaml
apiVersion: v1
kind: Pod
metadata:
  name: counter
  annotations:
    k8slog: "true" # 开启日志收集
spec:
  containers:
    - name: count
      image: busybox
      args:
        [
          /bin/sh,
          -c,
          'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done',
        ]

同样直接应用上面的资源清单文件即可:

$ kubectl apply -f counter.yaml

正常该 Pod 的日志就会被 Fluentd 收集到 OpenObserve 中了,我们可以在 OpenObserve 的 UI 界面中查看该日志流:

img

在日志页面选择 K8sLogs 流就可以查看到日志数据了:

img

指标

OpenObserve 除了支持日志之外,也支持指标数据的摄取,它支持 Prometheus 的远程写入协议,这样我们就可以直接将 Prometheus 的数据远程写入到 OpenObserve 中了。

下面的资源清单就是一个简单的 Prometheus 示例,我们使用 node_exporter 来采集节点的指标数据,然后通过 Prometheus 将其远程写入到 OpenObserve 中:

# prometheus.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: openobserve
data:
  prometheus.yaml: |
    global:
      scrape_interval: 15s
      scrape_timeout: 15s
    remote_write:  # 写入到远程 OO,url 是远程写入接口地址
    - url: http://openobserve.openobserve.svc.cluster.local:5080/api/default/prometheus/api/v1/write
      basic_auth:
        username: root@example.com
        password: root321
      # queue_config:    # 如果 Prometheus 抓取指标很大,可以加调整 queue,但是会提高内存占用
      #   max_samples_per_send: 10000  # 每次发送的最大样本数
      #   capacity: 20000
      #   max_shards: 30   # 最大分片数,即并发量。
    scrape_configs:
    - job_name: "nodes"
      static_configs:
      - targets: ['10.206.16.6:9100', '10.206.16.5:9100', '10.206.16.10:9100']
      relabel_configs: # 通过 relabeling 从 __address__ 中提取 IP 信息,为了后面验证 VM 是否兼容 relabeling
      - source_labels: [__address__]
        regex: "(.*):(.*)"
        replacement: "${1}"
        target_label: 'ip'
        action: replace
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus
  namespace: openobserve
spec:
  selector:
    matchLabels:
      app: prometheus
  template:
    metadata:
      labels:
        app: prometheus
    spec:
      containers:
        - args:
            - --config.file=/etc/prometheus/prometheus.yaml
            - --storage.tsdb.path=/prometheus
            - --storage.tsdb.retention.time=4h
            - --web.enable-lifecycle
          image: prom/prometheus:v2.44.0
          imagePullPolicy: IfNotPresent
          name: prometheus
          ports:
            - containerPort: 9090
              name: http
              protocol: TCP
          securityContext:
            runAsUser: 0
          volumeMounts:
            - mountPath: /etc/prometheus
              name: config-volume
            - mountPath: /prometheus
              name: data
      volumes:
        - name: data
          emptyDir: {}
        - configMap:
            defaultMode: 420
            name: prometheus-config
          name: config-volume
---
apiVersion: v1
kind: Service
metadata:
  name: prometheus
  namespace: openobserve
spec:
  ports:
    - name: http
      port: 9090
      targetPort: 9090
  selector:
    app: prometheus
  type: NodePort

上面的资源清单文件中,我们使用了 Prometheus 的 remote_write 配置项来将数据远程写入到 OpenObserve 中,其中 url 参数指定了远程写入接口地址,usernamepassword 参数指定了管理员的邮箱和密码。

直接应用上面的资源清单文件即可:

$ kubectl apply -f prometheus.yaml
$ kubectl get pods -n openobserve
NAME                          READY   STATUS    RESTARTS   AGE
openobserve-0                 1/1     Running   0          2d18h
prometheus-756c8c78f5-kvvbl   1/1     Running   0          20s
$ kubectl get svc -n openobserve
NAME          TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
openobserve   ClusterIP   None            <none>        5080/TCP         2d18h
prometheus    NodePort    10.107.32.131   <none>        9090:31019/TCP   37s

部署后我们依然可以使用 Prometheus 的 UI 界面来查看指标数据:

img

正常现在 Prometheus 的指标数据就会被写入到 OpenObserve 中了,我们可以在 OpenObserve 的 UI 界面中查看该指标流:

img

可以看到 OpenObserve 中是将每个指标看成一个独立的 stream 流来进行管理的,这样无疑大大增加了对指标数据管理的灵活性,但要想针对某个 job 进行管理难度也就大大增加了。现在我们就可以在 OpenObserve 的 UI 界面中查看指标数据了,比如查询 node_load5 指标:

img

可以和 Prometheus 中的查询结果进行对比:

img

从图形中可以看到 OpenObserve 的查询结果和 Prometheus 的查询结果是一致的。但是目前 OpenObserve 的 UI 界面中支持的 promql 语法还比较有限,比如不支持向量运算等操作(本周发布的版本将支持了)。

img

此外我们也可以使用 SQL 语法来查询指标数据,比如查询 node_load5 指标:

img

除了使用 Prometheus 的远程写入方式之外,OpenObserve 还支持通过 OpenTelemetry Collector(后面会讲解)来写入指标数据,只需要在 exporters 中配置 prometheusremotewrite 即可,如下所示配置:

exporters:
  prometheusremotewrite:
    endpoint: "http://<oo-url>/api/org_name/prometheus/api/v1/write"
    headers:
      Authorization: Basic base64_encoded_data_of(userid:password)

在指标页面查询数据的时候我们还可以将查询结果保存为 Dashboard:

img

在 Dashboard 里面还可以添加变量,比如我们这里添加一个变量 instance

img

然后编辑 Panel,将查询语句中的 instance 替换成 $instance

img

然后就可以在 Dashboard 中选择不同的 instance 来查看不同的指标数据了:

img

链路追踪

OpenObserve 除了支持日志和指标之外,还支持链路追踪,OpenObserve 遵循 OpenTelemetry 的追踪标准,我们可以使用通过 OpenTelemetry SDK 检测的代码将 Trace 数据发送到 OpenObserve,或通过自动检测将跟踪发送到 OpenObserve。

下面是一个集成 OpenTelemetry SDK 的 Python 示例,代码位于 git clone https://github.com/openobserve/sample-tracing-python

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter


# Service name is required for most backends
resource = Resource(attributes={
    SERVICE_NAME: "python-service"
})

# create a tracer provider
tracer_provider = TracerProvider(resource=resource)

# create an OTLP trace exporter
url = 'HTTP_Endpoint'
headers = {"Authorization": "Authorization"}

exporter = OTLPSpanExporter(endpoint=url, headers=headers)

# create a span processor to send spans to the exporter
span_processor = BatchSpanProcessor(exporter)

# add the span processor to the tracer provider
tracer_provider.add_span_processor(span_processor)

# set the tracer provider as the global provider
trace.set_tracer_provider(tracer_provider)

我们只需要将 HTTP_EndpointAuthorization 替换成 OpenObserve 的地址和管理员的认证信息即可。其中地址为 https://url:5080/api/<orgname>/tracesAuthorizationBasic base64(userid:password)。然后运行上面的代码即可将 Trace 数据发送到 OpenObserve 中。

除此之外 OpenObserve 还支持通过 OpenTelemetry Collector 来收集链路追踪数据,只需要在 OpenTelemetry Collector 的配置文件中配置 otlp 输出即可,如下所示:

# ......
exporters: # 导出器,用于导出数据
  jaeger:
    endpoint: "jaeger:14250" # 使用 jaeger 容器名称
    tls:
      insecure: true # 不使用 TLS

  # Data sources: traces, metrics
  otlphttp:
    traces_endpoint: http://url:5080/api/<orgname>/traces # OpenObserve 的接口地址,这里要用 traces_endpoint
    headers:
      Authorization: Basic base64(userid:password) # 管理员的认证信息

service: # 服务配置
  pipelines: # 管道配置
    traces: # 链路追踪配置
      receivers: [otlp] # 接收器
      exporters: [otlphttp] # 导出器
      processors: [batch] # 处理器

然后我们在应用中只需要通过 OpenTelemetry SDK 配置 OTLPTraceExporter 地址为 OpenTelemetry Collector 的地址即可,正常链路追踪数据就可以被收集到 OpenObserve 中去了。

如下图所示,我们可以在 OpenObserve 的 UI 界面中查看链路追踪数据:

img

点击某个 Trace 可以查看详细信息:

img

关于如何使用 OpenTelemetry 来生成并收集 Traces 数据、OpenTelemetry Collector 的部署等知识点我们后面会详细讲解。

报警

OpenObserve 支持两种警报方式,针对指定流进行评估:

  • 计划报警
  • 实时报警

要使用报警我们首先需要创建一个报警模板,当发送警报通知时使用模板,模板构建发送到目的地的请求正文,例如。对于 slack,可以创建如下模板:

{
  "text": "For stream {stream_name} of organization {org_name} alert {alert_name} of type {alert_type} is active"
}

在报警页面先添加一个报警模板,如下所示:

img

发送通知时,OpenObserve 会将 {stream_name}{org_name} 等占位符替换为 streamalertorganization 的实际值。可以在模板中使用的变量有:

变量 描述
stream_name 流名称 报警创建的流名称
org_name 组织名 组织名称
alert_name 报警名 报警名称
alert_type 报警类型 可以取的值 : real time 或者 scheduled

然后接下来需要创建一个 Destinations 目的地,用于发送警报通知,创建后可以在多个报警中使用。

比如我们这里创建一个 Slack 目的地,用于发送警报通知,如下所示:

img

关于 Slack 如何创建 Webhook 可以参考 Slack 官方文档

最后我们就可以创建一个报警了,如下所示:

img

这里我们添加了一个计划报警,当 K8sLogs 日志流在 1 分钟内的日志数量大于 50 条时就会触发报警,然后发送到 Slack 目的地中,需要注意的是这里查询语句最终会加上时间范围进行过滤,比如我们这里的查询语句在执行的时候实际执行的语句如下所示:

select count(*) as echocnt FROM 'K8sLogs' WHERE (_timestamp >= 1691488182902275 AND _timestamp < 1691488242902275)  LIMIT 100

正常情况下我们可以在 Slack 中看到报警信息:

img

另外对于实时报警是根据指定的条件在摄取时进行评估:

img

函数

OpenObserve 中的函数是使用 Vector Remap Language (vrl) 定义的,并且可以在数据摄取或查询期间用于帮助实现高级功能,例如增强、遮蔽、日志减少、合规性等。还有内置的查询函数,如 match_allmatch_all_ignore_case 等,可用于根据用户的流设置或默认设置进行全文搜索。

Vector Remap Language (VRL) 是一种面向表达式的语言,专门设计用于以安全和高效的方式转换可观察性数据(日志和指标)。它具有简单的语法和丰富的内置函数集,专门针对可观察性用例而设计。

可以使用 VRL Playground 来调试

img

del(.kubernetes_pod_ip)

parsed, err = parse_json(.log)

if err == null {
    .source = parsed.source
    .event = parsed.event
    .cycle = parsed.cycle

    del(.log)
}

......(当前版本有 bug)

高可用模式

前面我们了解到 OpenObserve 的架构支持单节点和 HA 两种模式,接下来我们来了解下 OpenObserve 的 HA 模式是如何使用的。

img

OpenObserve 可以在裸机服务器、虚拟机、Kubernetes 和其他平台上以 HA 模式安装和运行,但目前官方提供的使用 Helm Chart 的安装方式,所以需要提前准备一个可用的 Kubernetes 集群。

由于 HA 模式不支持本地磁盘存储,因此必须配置对象存储(例如 s3、minio、gcs 等等),这里我们就以 minio 为例进行说明。

首先添加 openobserve 的 helm 仓库:

$ helm repo add openobserve https://charts.openobserve.ai
$ helm repo update
# 或者直接 clone chart 仓库
$ git clone https://github.com/openobserve/openobserve-helm-chart.git

然后可以使用下面的命令来获取 helm chat 包:

$ tree openobserve-helm-chart
openobserve-helm-chart
├── Chart.lock
├── Chart.yaml
├── LICENSE
├── README.md
├── charts
│   ├── etcd-8.10.1.tgz
│   ├── etcd-8.11.4.tgz
│   └── minio-5.0.7.tgz
├── index.yaml
├── publish.sh
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── alertmanager-deployment.yaml
│   ├── alertmanager-service.yaml
│   ├── compactor-deployment.yaml
│   ├── compactor-hpa.yaml
│   ├── compactor-service.yaml
│   ├── configmap.yaml
│   ├── ingester-hpa.yaml
│   ├── ingester-service.yaml
│   ├── ingester-statefulset.yaml
│   ├── ingress.yaml
│   ├── issuer.yaml
│   ├── querier-deployment.yaml
│   ├── querier-hpa.yaml
│   ├── querier-service.yaml
│   ├── router-deployment.yaml
│   ├── router-hpa.yaml
│   ├── router-service.yaml
│   ├── secret.yaml
│   ├── serviceaccount.yaml
│   ├── zplane-deployment.yaml
│   ├── zplane-ingress.yaml
│   └── zplane-service.yaml
└── values.yaml

2 directories, 34 files

整个包里面还包括 etcd 和 minio 两个依赖的 helm chart 包,创建一个 oo-values.yaml 文件,然后添加如下内容:

auth:
  ZO_ROOT_USER_EMAIL: "root@example.com"
  ZO_ROOT_USER_PASSWORD: "root321"

  # 启用了 minio 不用设置这个
  # ZO_S3_ACCESS_KEY: "console"
  # ZO_S3_SECRET_KEY: "console123"

config:
  # ZO_S3_SERVER_URL: "http://minio:9000"  # 启用了 minio 不用设置这个
  ZO_S3_BUCKET_NAME: "openobserve"
  ZO_S3_REGION_NAME: "us-east-1"
  ZO_S3_PROVIDER: "minio" # 只有在使用 minio 进行对象存储时才需要设置这个,如果启用了minio,将会自动设置。
  ZO_TELEMETRY: "false" # 发送匿名遥测信息以改进OpenObserve,您可以将其设置为false来禁用。
  # ZO_ETCD_ADDR: "openobserve-etcd-headless.openobserve.svc.cluster.local:2379" # etcd endpoint,启用 etcd 会自动配置
  ZO_DATA_DIR: "./data/" # 指定数据目录,主要是 WAL 日志
  ZO_WAL_MEMORY_MODE_ENABLED: "false" # 开启内存模式,开启后不再写入本地文件,wal的数据直接在内存中,然后从内存转存到对象存储,为了解决某些云服务器,本地磁盘性能很差的问题,但是需要大内存。
  ZO_WAL_LINE_MODE_ENABLED: "true" # wal的写入模式,一般写入数据的时候都会批量写入,但是这有个潜在的风险。比如你一次写入了10k数据,如果我一次性写入wal,有可能遇到系统崩溃,掉电,这一批数据不知道断在哪儿,wal文件会出错。可能写入了一半。如果开启line模式,就是虽然你是一次给了我10k数据,我写入wal的时候,一行行的调用write接口,这样wal损坏的可能性小一些,但是性能稍微差一些。

replicaCount: # 可以根据需要调整副本数
  ingester: 1
  querier: 1
  router: 1
  alertmanager: 1
  compactor: 1

ingester:
  persistence: # 持久化 data 目录,主要是 WAL 日志
    enabled: true
    size: 10Gi
    storageClass: "cfsauto" # 指定可用的 storage class
    accessModes:
      - ReadWriteOnce

ingress:
  enabled: true
  className: "nginx"
  annotations:
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/connection-proxy-header: keep-alive
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-body-size: 32m
  hosts:
    - host: oo.k8s.local
      paths:
        - path: /
          pathType: ImplementationSpecific

etcd:
  enabled: true # 指定 etcd 是否启用
  replicaCount: 1 # 奇数
  persistence:
    size: 20Gi
    storageClass: "cfsauto" # 指定可用的 storage class
    accessModes:
      - ReadWriteOnce

minio:
  enabled: true # 指定 minio 是否启用
  region: "us-east-1"
  rootUser: rootuser
  rootPassword: rootpass123
  drivesPerNode: 1
  replicas: 2
  mode: distributed # or standalone
  buckets:
    - name: openobserve
      policy: none
      purge: false
  persistence:
    enabled: true
    size: 10Gi
    storageClass: "cfsauto" # 指定可用的 storage class
    accessModes:
      - ReadWriteOnce
  consoleIngress:
    enabled: true
    ingressClassName: "nginx"
    annotations:
      kubernetes.io/ingress.allow-http: "true"
      nginx.ingress.kubernetes.io/secure-backends: "true"
      nginx.ingress.kubernetes.io/whitelist-source-range: 0.0.0.0/0
    path: /
    hosts:
      - minio.k8s.local

在这个文件中我们指定了管理员的邮箱和密码,还有对象存储的相关配置,然后指定了 etcd 和 minio 的相关配置,要注意 authconfig 部分的配置,如果启用了 minio,那么 ZO_S3_ACCESS_KEYZO_S3_SECRET_KEYZO_S3_SERVER_URL 这些配置都可以省略,因为这些配置项会自动设置,包括如果启用了 etcd,ZO_ETCD_ADDR 参数也会自动配置。

另外我们可以看到 ingester 组件我们配置了数据持久化,这主要是为了对 WAL 日志数据的持久化,关于 WAL 主要有两种模式:

  • ZO_WAL_MEMORY_MODE_ENABLED: 内存模式,开启后不再写入本地文件,wal 的数据直接在内存中,然后从内存转存到对象存储,为了解决某些云服务器,本地磁盘性能很差的问题,但是需要大内存。
  • ZO_WAL_LINE_MODE_ENABLED: WAL 的写入模式,默认开启,一般写入数据的时候都会批量写入,但是这有个潜在的风险。比如你一次写入了 10k 数据,如果我一次性写入 wal,有可能遇到系统崩溃,掉电,这一批数据不知道断在哪儿,wal 文件会出错。可能写入了一半。如果开启 line 模式,就是虽然你是一次给了我 10k 数据,我写入 wal 的时候,一行行的调用 write 接口,这样 wal 损坏的可能性小一些,但是性能稍微差一些。

所以如果我们使用内存模式的话理论上可以不用持久化数据目录,但有一种情况会落盘,就是当内存中还有数据的时候程序接收到关闭指令,会把内存中的数据 dump 到磁盘,下次启动的时候再转移到对象存储中去。

更多配置可以参考官方文档关于环境变量的说明:https://openobserve.ai/docs/environment-variables/。

然后使用如下命令即可一件安装 OpenObserve:

$ helm upgrade --install openobserve -f oo-values.yaml --namespace openobserve ./openobserve-helm-chart
Release "openobserve" does not exist. Installing it now.
NAME: openobserve
LAST DEPLOYED: Thu Aug 10 15:31:37 2023
NAMESPACE: openobserve
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
1. Get the application URL by running these commands:
  http://oo.k8s.local/

安装后我们可以查看一下 OpenObserve 的 Pod 状态:

$ kubectl get pods -n openobserve
NAME                                        READY   STATUS    RESTARTS      AGE
openobserve-alertmanager-7957d8fb79-xzh8z   1/1     Running   4 (31m ago)   32m
openobserve-compactor-d679d4765-gpkgk       1/1     Running   4 (31m ago)   32m
openobserve-etcd-0                          1/1     Running   0             32m
openobserve-ingester-0                      1/1     Running   0             4m31s
openobserve-minio-0                         1/1     Running   0             32m
openobserve-minio-1                         1/1     Running   0             32m
openobserve-querier-56456d48c5-dth52        1/1     Running   4 (31m ago)   32m
openobserve-router-7bd4fcbc6c-nrmbs         1/1     Running   3 (32m ago)   32m
prometheus-756c8c78f5-gkwl8                 1/1     Running   0             2d
$ kubectl get svc -n openobserve
NAME                        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)             AGE
openobserve-alertmanager    ClusterIP   10.102.247.215   <none>        5080/TCP            71m
openobserve-compactor       ClusterIP   10.96.24.228     <none>        5080/TCP            71m
openobserve-etcd            ClusterIP   10.103.96.225    <none>        2379/TCP,2380/TCP   71m
openobserve-etcd-headless   ClusterIP   None             <none>        2379/TCP,2380/TCP   71m
openobserve-ingester        ClusterIP   10.108.17.186    <none>        5080/TCP            71m
openobserve-minio           ClusterIP   10.108.196.221   <none>        9000/TCP            71m
openobserve-minio-console   ClusterIP   10.103.65.90     <none>        9001/TCP            71m
openobserve-minio-svc       ClusterIP   None             <none>        9000/TCP            71m
openobserve-querier         ClusterIP   10.99.221.44     <none>        5080/TCP            71m
openobserve-router          ClusterIP   10.101.230.112   <none>        5080/TCP            71m
$ kubectl get ingress -n openobserve
NAME                        CLASS   HOSTS             ADDRESS       PORTS   AGE
openobserve                 nginx   oo.k8s.local      10.98.12.94   80      53m
openobserve-minio-console   nginx   minio.k8s.local   10.98.12.94   80      53m

可以看到 OpenObserve 的几个组件 Router、Querier、Ingester、Alertmanager、Compactor、Minio、Etcd 都已经正常运行了,此外我们为 MinIO 和 OpenObserve 都添加了 Ingress,只需要将 oo.k8s.localminio.k8s.local 映射到 Ingress 控制器即可访问。

比如现在我们可以通过 minio.k8s.local 来访问 MINIO 的 UI 界面:

img

使用用户名 rootuser 和密码 rootpass123 即可登录成功。

可以看到指定的 openobserve bucket 也已经创建成功了:

img

同样我们也可以通过 oo.k8s.local 来访问 OpenObserve 的 UI 界面:

img

只是现在还没有任何数据:

img

接下来我们只需要将前面日志、指标、链路追踪的数据发送到新的 OpenObserve 地址 http://openobserve-router.openobserve.svc.cluster.local:5080 即可,比如前面我们已经部署的 Fluentd,只需要将日志输出地址修改即可:

# fluentd.yaml
# ...... 省略部分配置
output.conf: |-
  <match **>
    @type http # 使用http插件
    endpoint http://openobserve-router.openobserve:5080/api/default/K8sLogs/_json # 指定接收日志的地址
    content_type json
    json_array true
    <auth>
      method basic
      username root@example.com
      password root321
    </auth>
    <buffer>
      flush_interval 2s   # 每2秒发送一次
    </buffer>
  </match>

然后重启 fluentd 即可,隔一会儿就可以在 OpenObserve 的 UI 界面中看到日志数据了:

img

同样对于 Prometheus 也是一样的,只需要修改 Prometheus 的配置文件中的远程写入地址即可:

remote_write: # 写入到远程 OO,url 是远程写入接口地址
  - url: http://openobserve-router.openobserve:5080/api/default/prometheus/api/v1/write
    basic_auth:
      username: root@example.com
      password: root321

img

对于链路追踪数据也是一样的,只需要修改 OpenTelemetry Collector 的配置文件中的远程写入地址即可。

最后我们可以去 MINIO 中验证下数据是否已经写入到对象存储中了:

img

img

不过需要注意的是数据并不是实时上传到对象存储中的,默认 10 分钟或者 WAL 达到 32MB 后会上传一次。

到这里 OpenObserve 的 HA 模式就部署完成了,我们可以根据需要设置各个组件的副本数量进行横向扩展,也可以启用 HPA 来自动扩缩容。44.OpenObserve