Export over OpenTelemetry

asz push sends what the storage root holds to an OpenTelemetry logs receiver: every landed file and every round, over OTLP/HTTP with a protobuf body, to the receiver’s /v1/logs. The SkyWalking OAP accepts it on its REST port, and so does an OpenTelemetry Collector.

# asz.yaml
export:
  otlp:
    endpoint: http://127.0.0.1:12800
./bin/asz push -once      # send everything not yet sent, then exit
./bin/asz push            # keep sending new files every export.otlp.interval

One log record per line

A file is sent as one log record per line, the header and the closing line included, and each record’s body is the line’s bytes unchanged. That is what lets a receiver write the file back byte for byte: the file’s digest still matches, an uploaded round still verifies against the landed files it names, and every {seq, row, block} reference still means what it meant.

Landed files and rounds are both write-once, so each is sent once. push.state in the storage root lists what was sent, with the digest each file had; a file is recorded only after the request carrying its last line succeeded, so a failed request leaves it for the next pass.

What every record carries

The resource, which names the service a record belongs to:

Attribute Value
service.name export.otlp.service_name, or when empty the project directory the session was recorded under, one service per project
service.instance.id the session id
service.layer export.otlp.layer, GENAI by default, the OAP’s layer for generative AI
telemetry.sdk.name asz, so a receiver can tell these records apart from any other source
telemetry.sdk.version the version of asz that sent them
telemetry.sdk.language go

The scope is github.com/apache/skywalking-ai-sessionizer with the same version. Each record then says what it is:

Attribute Value
asz.format sd for a landed file, sf for a round
asz.format.version the version in the file’s first line: sd/1 or sf/1
asz.file.kind for sd, the header’s kind: transcript, agent_meta, journal, workflow_manifest, workflow_script; for sf, round
asz.file the file’s path relative to the storage root
asz.file.digest the file’s SHA-256, so a receiver can check what it wrote back
asz.line the line’s index in the file, starting at 0 for the header
asz.line.kind for sd: header, record, end; for sf: the frame type, header, node, relation, unresolved, commit
asz.session the session
asz.stream, asz.run, asz.seq for sd: the stream or workflow run, and the landed sequence
asz.conversation, asz.round for sf: the conversation and the round number

A landed record is stamped with its own time when it carries one, and with the file’s collection time otherwise. A round carries no time of its own, by design, so its lines are stamped with the time they were sent. Every record also carries the time it was observed.

Checking what a receiver gets

An OpenTelemetry Collector with a file exporter writes back what it decoded, which is the easiest way to see the records before pointing at a backend:

receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
exporters:
  file:
    path: /out/logs.json
service:
  pipelines:
    logs:
      receivers: [otlp]
      exporters: [file]

Point export.otlp.endpoint at http://127.0.0.1:4318, run asz push -once, and read logs.json: one JSON line per request, with the resource, the scope and the records as the Collector understood them.

Size

A request carries at most export.otlp.batch_bytes of body text, 1 MiB by default, and a file larger than that is sent across several requests in order. The largest single record in the measured corpus is 4.5 MB and is sent alone. The OAP accepts requests up to 50 MB by default; an OpenTelemetry Collector accepts 4 MiB, which is why the landing budget defaults to 4 MiB as well.