如何使用 advancedSink 和 rawTransforms 转发日志

类型化的 LogForward 输出(kafka、elasticsearch、opensearch、syslog、http、console、blackhole、clickhouse)涵盖了大多数转发场景。advancedSink 输出则是处理其他所有情况的备用方案——它允许你按名称选择任意 Vector sink,并在 sink 写入之前,使用一组经过允许列表限制的少量 Vector transforms重新组织事件。

在以下情况下使用 advancedSink:

  • 目标端是平台未作为类型化输出公开的供应商 sink(datadog_logs、humio_logs、aws_s3、nats、gcp_cloud_storage 等);
  • 目标端需要类型化输出不会生成的特定线路格式封装(OTLP resourceLogs/scopeLogs/logRecords、Splunk HEC 批次等);
  • 转发路径需要比平台过滤器(parse、filter、redact、prune、remapFields)更丰富的事件级重组能力——例如,将多条记录批量聚合到一个请求正文中,或编写组合多个条件的 VRL 谓词。

如果以上情况均不适用,请优先使用类型化输出和如何使用 LogForward 转发日志中介绍的标准过滤器——它们的验证更严格,也更易于审查。

前提条件

  • 已安装 Alauda Container Platform Log Storage Management,并且 PlatformLogForward/platform-default 为 Ready。请参阅安装。
  • v4.4 版本中提供的存储 operator 包含 advancedSink.rawTransforms 字段,并且不会阻止在参数中使用 batch / request。本文档中的模式假定你使用的是该版本。
  • spec.output.authSecretRef 和 spec.output.tls.caSecretRef 引用的 Secret 位于 cpaas-system 中。
Warning

LogForward 仅位于 cpaas-system 与其他所有 LogForward 一样存在相同限制:admission webhook validate-lf.log.alauda.io 会拒绝 LogForward 位于 cpaas-system 之外的任何情况。每个 CR 都由平台管理员而非租户编写。

Spec 结构

spec:
  inputs:       [...]           # same as a typed LF
  filters:      [...]           # optional — platform-typed filters, field-level ops
  pipelines:    [...]           # same as a typed LF, wires inputs → filters → the single output
  output:
    type: advancedSink
    endpoint:  <optional>       # some sinks use endpoint; others carry URL inside parameters
    authSecretRef:              # optional, Secret keys: username / password
      name: <secret>
    tls:                        # optional
      verify: true | false
      caSecretRef:
        name: <secret>          # Secret key: ca.crt
    advancedSink:
      componentType: <string>   # one Vector sink type, e.g. http / datadog_logs / humio_logs / aws_s3
      parameters:   <object>    # Vector sink config fragment (keys mirror Vector's TOML)
      rawTransforms: [...]      # optional — a chain of Vector transforms before the sink

advancedSink.componentType

按名称选择恰好一个 Vector sink 类型。名称与 Vector 自身文档中的名称一致,例如 http、datadog_logs、humio_logs、aws_s3、nats、elasticsearch(如果你希望使用其完整的配置项,而不是平台的类型化封装)。存储 operator 不维护按 sink 划分的允许列表,因为新版本的 Vector 添加 sink 的速度快于任何 Curated 列表的更新速度。Vector 会在启动时验证类型;如果名称错误,或 sink 依赖的 Vector feature flag 已关闭,Vector pod 将无法启动,并且 LogForward 会报告 Ready=False,其中包含针对 status.conditions 的解析器错误。

advancedSink.parameters

一个自由格式的对象,其键与所选 sink 的 Vector 配置相对应。嵌套对象会转换为嵌套 TOML 表({"encoding": {"codec": "json"}} 会变为 [sinks.output.encoding] codec = "json")。

保留键(admission webhook 会拒绝参数顶层的以下键):

键保留原因
typeoperator 已根据 componentType 设置该键。
inputsoperator 会根据 pipelines 和 rawTransforms 自行连接输入。
sources / transforms / route单个 sink 块不能承载子拓扑。
buffer缓冲区行为属于平台契约。
acknowledgements / healthcheck平台决定 ack 和健康检查机制。

名称相同的嵌套键(例如 {"resource": {"type": "gce_instance"}})不会被拒绝——webhook 只检查参数顶层。

之前保留、现在允许:batch 和 request。两者都是按 sink 设置的配置项;下面的 OTLP 封装方案需要使用 batch.max_events: 1 才能正常工作。

advancedSink.rawTransforms[]

一个可选的、有序的 Vector transforms 链,在平台过滤器之后、sink 写入之前运行。每个条目包含:

字段类型说明
name字符串,在 LF 内唯一,[A-Za-z0-9_-]+,长度为 1–63 个字符Transform ID。
type枚举:remap、filter、reduce、throttle、sample、dedupeVector transform 类型。
spec对象Transform 的 Vector 配置片段(键与 Vector 的 TOML 对应;由于 operator 会连接它们,因此 inputs 和 type 会被拒绝)。

为什么是这六种类型?它们是本地、无副作用的 transforms——不会更改拓扑,不会创建子输出,也不会调用外部服务。route(多输出)会破坏 operator 的输入连接。lua(任意代码)存在安全风险。log_to_metric 应用于 metrics pipeline。

生成的 Vector pipeline 中的执行顺序:

Kafka source → platform filters (parse/filter/redact/prune/remapFields)
             → rawTransforms[0] → rawTransforms[1] → … → sink

operator 会在每个阶段连接 inputs;你无需自行编写 inputs(如果编写,webhook 也会拒绝)。

在平台 filters[] 条目和 rawTransforms[] 条目之间进行选择

两者存在重叠。经验法则如下:

需求使用
重命名、删除或保留一组扁平字段;移除已知的 PII 字段;解析 JSON message;根据简单的字段相等条件、子字符串或正则表达式进行过滤平台类型化过滤器(parse / filter / redact / prune / remapFields)。验证更严格,且不使用 VRL。
重塑事件(扁平结构 → 嵌套结构、构建 OTLP 记录、封装为批次);在单个 VRL 表达式中编写组合多个字段的谓词;按键限流;在保留所有错误的同时进行采样;在滑动窗口内去重rawTransforms 条目(remap、filter、reduce、throttle、sample、dedupe)。

可以自由组合使用:平台过滤器先运行,完成简单的字段级处理;然后由 rawTransforms 对已清理的事件执行更复杂的结构处理。

食谱

三个完整示例,从简单到复杂。

食谱 A — 将审计记录转发到 Datadog,同时进行裁剪和丰富

场景:将平台审计记录发送到 Datadog Logs。使用平台的 prune 过滤器删除噪声字段,然后使用 rawTransforms.remap 添加 Datadog 可用于路由的租户标签。

apiVersion: log.alauda.io/v1alpha1
kind: LogForward
metadata:
  name: audit-to-datadog
  namespace: cpaas-system
spec:
  replicas: 2
  inputs:
    - name: audit-in
      type: audit
      initialOffset: Latest
  filters:
    - name: strip-noise
      type: prune
      prune:
        deny: [pod_labels, node_name, path]      # platform-level, field-only
  pipelines:
    - name: audit-pipe
      inputRefs: [audit-in]
      filterRefs: [strip-noise]
  output:
    type: advancedSink
    authSecretRef:
      name: user-datadog-api                     # Secret keys: username(ignored) / password(API key)
    advancedSink:
      componentType: datadog_logs
      parameters:
        site: datadoghq.com
        default_api_key: "${OUTPUT_PASSWORD}"    # vector expands at startup from Secret
        compression: gzip                        # vector's datadog_logs sink serialises
                                                 # as JSON on wire; do NOT set
                                                 # `encoding.codec` here — this sink's
                                                 # `encoding` block only accepts
                                                 # only_fields / except_fields /
                                                 # timestamp_format, and `codec` makes
                                                 # `vector validate` fail at startup.
      rawTransforms:
        - name: add-tenant-tag
          type: remap
          spec:
            source: |
              .ddtags = "tenant:acme,env:prod,cluster:" + (to_string(.cluster) ?? "unknown")
              .service = "acp-audit"

需要注意的事项:

  • authSecretRef.password → Vector pod 上的 OUTPUT_PASSWORD 环境变量 → parameters 中的 ${OUTPUT_PASSWORD} 字面量会在 Vector 启动时展开。username 键是 Secret 结构所必需的,但 Datadog 不会读取它。
  • 平台的 prune 过滤器在 remap 之前运行,因此丰富处理 VRL 处理的是已经缩减的事件。
  • VRL 中可能失败的操作(to_string(x))必须包含回退值(?? "unknown"),否则整个 transform 会中止该事件。完整的 VRL 食谱集请参阅 operator 侧指南:log-storage-operator/docs/HOWTO_LF_ADVANCED_SINK_RAW_TRANSFORMS.md。

食谱 B — 将平台日志转发到通用 HTTP 接收端,同时使用 VRL 过滤器和采样

场景:将应用日志发送到通用 HTTP 接收端,但使用平台 filter 无法表示为单个谓词的复合条件来删除健康检查噪声,并以每个 pod 10% 的比例对保留的流进行采样,以控制数据量。

apiVersion: log.alauda.io/v1alpha1
kind: LogForward
metadata:
  name: app-logs-to-http
  namespace: cpaas-system
spec:
  replicas: 2
  inputs:
    - name: platform-log
      type: log
      initialOffset: Latest
      selector:
        namespaces:
          include: [prod-app-1, prod-app-2]
  filters:
    - name: redact-tokens
      type: redact
      redact:
        fields: [access_token, bearer_token]
        replacement: "[REDACTED]"
  pipelines:
    - name: to-intake
      inputRefs: [platform-log]
      filterRefs: [redact-tokens]
  output:
    type: advancedSink
    advancedSink:
      componentType: http
      parameters:
        uri: https://intake.example.com/logs
        method: post
        encoding:
          codec: json
        framing:
          method: newline_delimited
        request:
          headers:
            Authorization: "Bearer <paste-token-or-use-authSecretRef>"
            Content-Type: application/x-ndjson
      rawTransforms:
        # 1) drop healthcheck noise AND warnings whose message is just a stacktrace tail
        - name: drop-noise
          type: filter
          spec:
            condition: |
              msg   = to_string(.message) ?? ""
              level = to_string(.level) ?? ""
              # keep when the condition is true
              level != "debug" &&
              !contains(msg, "/healthz") &&
              !contains(msg, "/readyz")  &&
              !(level == "warn" && starts_with(msg, "\tat "))
        # 2) sample 10% per pod — keep same pod's events consistent inside the window
        - name: sample-10pct
          type: sample
          spec:
            rate: 10
            key_field: ".kubernetes_pod_name"
            exclude: '.level == "error"'           # errors are always kept

需要注意的事项:

  • 平台的 redact 过滤器与 rawTransforms filter / sample 组合使用:先进行脱敏,然后删除,最后采样。
  • sample 上的 exclude 字段可确保所有错误都得以保留,这是常见需求,但无法通过平台的 filter 过滤器表达。
  • 如果同时设置了 authSecretRef 和 parameters.request.headers.Authorization,Vector 的 http sink 将拒绝启动。请选择其中一种路径(与如何使用 LogForward 转发日志中的类型化 HTTP 输出规则相同)。

食谱 C — 将平台日志作为 OTLP 信封转发到 OpenTelemetry Collector

场景:将平台日志发送到 OTel Collector 的 otlphttp receiver。OTLP/HTTP 需要单个顶层 JSON 对象({resourceLogs: [...]}),而不是数组——因此该食谱使用 remap + reduce + remap 构建信封,并在 sink 上设置 batch.max_events: 1,使 Vector 不会将生成的单个信封重新批处理为数组。

apiVersion: log.alauda.io/v1alpha1
kind: LogForward
metadata:
  name: logs-to-otlp
  namespace: cpaas-system
spec:
  replicas: 2
  inputs:
    - name: platform-log
      type: log
      initialOffset: Latest
  pipelines:
    - name: to-otlp
      inputRefs: [platform-log]
  output:
    type: advancedSink
    advancedSink:
      componentType: http
      parameters:
        uri: https://otel-collector.example.com:4318/v1/logs
        method: post
        encoding:
          codec: json
        framing:
          method: bytes
        request:
          headers:
            Content-Type: application/json
        batch:
          max_events: 1              # one reduce output → one POST body (object, not array)
      rawTransforms:
        # 1) per-event: build one OTLP logRecord and attach a batch key for the reducer
        - name: to-record
          type: remap
          spec:
            source: |
              body_str, _ = to_string(.message)
              if body_str == null || body_str == "" {
                body_str = encode_json(.)
              }
              rec = {
                "timeUnixNano": "0",
                "severityText": to_string(.level) ?? "INFO",
                "body":         { "stringValue": body_str },
                "attributes": [
                  { "key": "k8s.namespace.name", "value": { "stringValue": to_string(.kubernetes_namespace) ?? "" } },
                  { "key": "k8s.pod.name",       "value": { "stringValue": to_string(.kubernetes_pod_name)   ?? "" } },
                  { "key": "k8s.container.name", "value": { "stringValue": to_string(.container_name)        ?? "" } }
                ]
              }
              . = { "batchKey": "ALL", "rec": rec }
        # 2) batch many records into one event with a .rec array
        - name: wrap
          type: reduce
          spec:
            group_by:        [batchKey]
            expire_after_ms: 1000       # one POST per second
            merge_strategies:
              rec: array
        # 3) shape the final OTLP envelope
        - name: envelope
          type: remap
          spec:
            source: |
              recs = array!(.rec)
              . = {
                "resourceLogs": [{
                  "resource": {
                    "attributes": [
                      { "key": "service.name", "value": { "stringValue": "acp-log-forward" } }
                    ]
                  },
                  "scopeLogs": [{
                    "scope":      { "name": "acp-lf", "version": "0.1" },
                    "logRecords": recs
                  }]
                }]
              }

需要注意的事项:

  • 对于 OTLP/HTTP,batch.max_events: 1 是必需的。如果没有它,Vector 的 http sink 会将 reducer 的单个信封包装为单元素数组 [{...}],而 OTel Collector 会以 400 错误拒绝该请求。
  • expire_after_ms 用延迟换取批次大小。1 秒(如示例所示)是合理的默认值;在数据量非常大时将其降低到 500 ms,在数据量非常小时将其提高到 5 000 ms。关注 Collector 的 max_request_body_size——如果信封超过该限制,请缩短 expire_after_ms。
  • 时间戳(timeUnixNano: "0")是占位符:Collector 将使用接收时间。若要保留真实事件时间,请将其替换为 to_string(to_unix_timestamp(parse_timestamp!(.timestamp, "%+")) * 1000000000)。
  • 如果 Collector 需要身份验证,请将其添加到 parameters.request.headers.Authorization(Bearer ${OUTPUT_PASSWORD}),并使用 Secret 的 password 中的密钥设置 authSecretRef。

验证

与任何 LogForward 相同:

kubectl -n cpaas-system apply -f my-advanced-lf.yaml
kubectl -n cpaas-system get logforward <name> -w
# wait for READY=True

如果未进入 Ready 状态,请读取条件:

kubectl -n cpaas-system get logforward <name> \
  -o jsonpath='{range .status.conditions[*]}{.type}{"\t"}{.status}{"\t"}{.reason}{"\t"}{.message}{"\n"}{end}'

特定于 advancedSink 的常见 Ready=False 原因:

  • AdvancedSinkParametersRejected — 参数顶层设置了保留键(type、inputs、sources、transforms、route、buffer、acknowledgements、healthcheck)。
  • RawTransformsRejected — 某个 rawTransforms[*] 条目包含不在允许列表中的 type、重复的 name,或者在其 spec 中写入了 inputs / type。
  • VectorValidateFailed — operator 会渲染 Vector 配置,将 vector validate 作为预检调用,并且仅在验证通过后才滚动更新 Deployment。条件消息会逐字包含 Vector 解析器错误;这几乎总是 Vector sink 字段名称拼写错误或 VRL 语法错误(fallible without ??)。

要调试 sink 实际发出的线上负载,请将 sink 的 uri 指向一个短期运行的 echo pod,然后读取接收到的内容——operator 端指南 log-storage-operator/docs/HOWTO_LF_ADVANCED_SINK_RAW_TRANSFORMS.md 提供了可直接复制的 echo pod 配方。

限制和注意事项

  • rawTransforms[*].type 仅限于 remap、filter、reduce、throttle、sample、dedupe。其他 Vector transform 类型(尤其是 route 和 lua)会被拒绝。
  • rawTransforms[*].spec 不能携带 inputs 或 type——这些由 operator 连接。
  • 一个 LogForward 只有一个输出和一个 Vector Deployment。要扇出到多个目标,请创建多个 LogForward CR;它们只能通过复制粘贴共享平台过滤器(不存在跨 CR 的过滤器复用)。
  • 平台过滤器(filters[])和 rawTransforms[] 都会运行;先运行过滤器,再运行 rawTransforms。字段级操作使用前者,事件形状操作使用后者。
  • 平台的 KafkaUser 必须包含 logforward- consumer-group 前缀 ACL 规则。没有该规则,Vector pod 会静默读取零条记录,而 LogForward 仍会报告 Ready=True。有关 ACL 配置方法,请参阅安装。
  • advancedSink 是一个逃生阀。Vector sink schema 可能会在 Vector 次要版本之间发生变化;operator 会在每次 reconcile 时将 vector validate 作为预检运行,并拒绝滚动更新损坏的配置,因此升级仍然安全,但存储 operator 进行 major 升级后,仍建议重新阅读 Vector sink 参考文档。

相关主题

  • 使用 LogForward 转发日志 — 类型化输出(kafka、elasticsearch、opensearch、syslog、http、console、blackhole、clickhouse)和平台过滤器目录。
  • 将日志归档到第三方存储 — razor consumer --database-type=file 归档路径,用于 NFS / S3 冷存储,而非实时转发。
  • 安装 — 存储 operator 的安装,以及 logforward- KafkaUser 前缀 ACL 规则。