Skip to main content

Stream CI build logs to your observability backend

Last updated on

When enabled, Harness CI can emit every build step log line as a structured JSON record to the build container's stdout, alongside the standard Harness Log Service. Once those records are written, you can collect and ship them to your observability backend using any log collector of your choice.

This gives you full ownership of your CI log data. You can search, alert on, correlate, and retain build logs in your own infrastructure without changing pipeline YAML and without coupling Harness to any specific telemetry stack.


What you will learn

  • What dual logging is: How the HARNESS_LOG_STREAMING_STDOUT_ENABLED flag enables a parallel stream that mirrors every build step log line as flat JSON.
  • Supported infrastructure: Which delegate and build infrastructure combinations are supported today and what is planned.
  • Collector setup: How to enable log streaming on your delegate and configure log collection for Kubernetes build infrastructure.
  • Log schema and queries: The exact JSON fields emitted on each line and example queries you can run in Grafana Loki, Splunk, or Elasticsearch.

Supported infrastructure

Support for CI log streaming depends on your delegate type and build infrastructure:

DelegateBuild infrastructureStatusHow logs are collected
Self-managed Legacy DelegateKubernetes clusterSupportedA node-level log collector DaemonSet tails build pod container logs.
Self-managed Kubernetes DelegateKubernetes clusterComing soonThis capability is currently planned for Kubernetes-based builds running on Delegate 3.x.
Harness Cloud (hosted build infrastructure)N/ANot applicableLogs are managed by Harness and are not exposed for external collection.
Kubernetes build support on new delegates is coming soon

For Kubernetes build infrastructures, log streaming currently supports the self-managed Legacy Delegate. Equivalent CI Kubernetes build log streaming for Delegate 3.x is coming soon.


How it works

When log streaming is enabled, the delegate propagates the flag to every container in every CI build pod it creates. The orchestrator container writes stage-level lines (such as setup, step dispatch, and teardown) as JSON to its own stdout, while each step container writes its stdout and stderr as JSON to its own stdout. Because every line is written to container stdout, the standard node-level path /var/log/pods/<namespace>_harnessci-*/**/*.log captures all records.

Harness emits JSON, not OTLP

The Harness execution engine only writes structured JSON to container stdout. It does not speak OTLP natively. The choice of log collector (such as OpenTelemetry Collector, Fluent Bit, Vector, or Promtail) and downstream protocol is entirely yours.


Before you begin

This guide assumes:

  • You have Continuous Integration enabled on your Harness account. Go to Harness CI overview to review account prerequisites and module setup.
  • You are using a self-managed Legacy Delegate with Kubernetes build infrastructure.
  • You have permission to update delegate manifests.
  • You have an observability backend that can receive logs (such as Grafana Loki, Splunk, Datadog, or Elasticsearch).

Step 1: Enable log streaming

Enable log streaming by setting the appropriate environment variables.

For Kubernetes build infrastructure (Legacy Delegate)

Edit your delegate Deployment manifest and add the environment variable HARNESS_LOG_STREAMING_STDOUT_ENABLED=true to the delegate container:

apiVersion: apps/v1
kind: Deployment
metadata:
name: harness-delegate
namespace: harness-delegate-ng
spec:
template:
spec:
containers:
- name: harness-delegate-instance
env:
- name: HARNESS_LOG_STREAMING_STDOUT_ENABLED
value: "true"

Apply the manifest and wait for the rollout to complete:

kubectl apply -f delegate.yaml
kubectl rollout status deploy/harness-delegate -n harness-delegate-ng

For Docker-based delegates

Add the environment variable to your docker run command:

# Replace the placeholders below with your actual delegate environment variables.
docker run -d \
-e HARNESS_LOG_STREAMING_STDOUT_ENABLED=true \
-e ACCOUNT_ID=<your-account-id> \
harness/delegate:<version>

Step 2: Configure log collection

Once log streaming is enabled, you can configure your log collector of choice to tail and forward the log records.

Deploy an OpenTelemetry (OTel) Collector DaemonSet in your cluster to tail the node container logs. The filelog receiver tails container logs under /var/log/pods, and a parser extracts the JSON envelope emitted by Harness CI.

Below is an example snippet for the OTel Collector configuration. Replace <your-namespace> and <your-delegate-name> with your actual values:

receivers:
filelog:
# Tail CI build pods (prefixed with harnessci-) and delegate execution logs
include:
- /var/log/pods/<your-namespace>_harnessci-*/**/*.log
- /var/log/pods/<your-namespace>_<your-delegate-name>-*/**/*.log
start_at: end
include_file_path: true
operators:
# Parse CRI log format: <time> <stream> <flags> <log>
- type: regex_parser
regex: '^(?P<time>[^ ]+) (?P<stream>stdout|stderr) (?P<flags>[^ ]+) (?P<log>.*)$'
on_error: send
timestamp:
parse_from: attributes.time
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
# Parse the nested JSON envelope written by the containers
- type: json_parser
parse_from: attributes.log
if: 'attributes["log"] matches "^\\{.*"'
on_error: send

processors:
batch:
timeout: 2s
send_batch_size: 500
resource:
attributes:
- key: service.name
value: harness-ci
action: insert

exporters:
otlphttp:
endpoint: http://<your-backend-endpoint>:3100/otlp

service:
pipelines:
logs:
receivers: [filelog]
processors: [batch, resource]
exporters: [otlphttp]
Other collectors work similarly

You can configure other collectors, such as Fluent Bit, Vector, or Promtail, to tail the same pod directories (/var/log/pods/) and parse the container logs as JSON before forwarding them to your backend.


Step 3: Verify log streaming

  1. Run a CI pipeline that uses your Kubernetes build infrastructure.
  2. Check your collector DaemonSet logs:
kubectl logs -l app=otel-collector -n <your-namespace> --tail=20
  1. Query your observability backend for logs with service.name = "harness-ci".

Log format reference

Each CI log line is written as a structured JSON object:

{
"timestamp": "2026-05-19T10:15:30.123456789Z",
"level": "INFO",
"message": "Successfully built image docker.io/myorg/myapp:latest",
"logType": "EXECUTION_LOGS",
"logAbstractions": {
"accountId": "abc123",
"orgId": "default",
"projectId": "my_project",
"pipelineId": "build_and_push",
"runSequence": "42",
"planExecutionId": "exec_abc123def456",
"stageIdentifier": "build_stage",
"stepIdentifier": "build_and_push_step"
},
"logContext": {
"taskId": "task_xyz789"
}
}

Field reference

FieldDescription
timestampUTC timestamp in RFC 3339 nanosecond format.
levelLog level: INFO, WARN, ERROR. Note that for Kubernetes build pod stdout, this is currently hardcoded to INFO on the stdout stream. Severity-based filtering can be done on the message content downstream or by relying on the original Harness Log Service.
messageThe actual log line content.
logTypeAlways EXECUTION_LOGS for CI step execution output.
logAbstractions.accountIdYour Harness account identifier.
logAbstractions.orgIdThe organization identifier.
logAbstractions.projectIdThe project identifier.
logAbstractions.pipelineIdThe pipeline identifier.
logAbstractions.runSequenceThe run number (or run sequence).
logAbstractions.planExecutionIdThe unique execution ID for this pipeline run.
logAbstractions.stageIdentifierThe stage that produced the log.
logAbstractions.stepIdentifierThe identifier of the step that produced this line, or engine for stage-level orchestration lines.
logContext.taskIdThe delegate task ID (present when available).
Delegate-level execution logs are slightly different

When log streaming is enabled, the delegate process itself also writes a JSON stream of general execution logs (such as CD task execution). Those records carry the same structure, plus two additional fields when available: logKey (the Harness internal log key) and commandUnit (the command unit name, such as Execute). These two fields are not present in standard CI step log records.


Query examples

The following query examples assume the default service.name tags configured in the steps above.

Grafana Loki (LogQL)

# Query all Kubernetes build logs
{service_name="harness-ci"}

# Filter by a specific pipeline
{service_name="harness-ci"} | json | logAbstractions_pipelineId="build_and_push"

# Filter for errors by inspecting message content
{service_name="harness-ci"} |~ "(?i)(error|failed|exception|panic)"

Splunk (SPL)

index=harness-ci sourcetype="_json"
| spath "logAbstractions.pipelineId"
| search "logAbstractions.pipelineId"="build_and_push"

Elasticsearch (KQL)

logAbstractions.pipelineId: "build_and_push" AND level: "ERROR"

Resiliency and data safety

Because log streaming writes logs in parallel to the standard flow, Harness UI logs are completely unaffected if your log collector or observability backend experiences an outage. The collector handles buffering and retries asynchronously.

Outage ScenarioCollector BehaviorData Impact
Log collector restartsThe collector tracks its read position (offset) in each log file. Upon restart, it resumes reading from that offset.No data lost (as long as files have not been rotated or pruned on disk).
Backend is temporarily downThe collector buffers logs in a sending queue and retries with exponential backoff.No data lost (within the buffer and retry window).
Backend is down for an extended periodThe sending queue becomes full. Once the queue is full, the oldest buffered log entries are dropped.Logs may be lost on the collector side (only the external copy; Harness UI logs remain fully intact).
Collector crashesIn-memory buffers and queue states are lost.Minimal data lost (limited to in-flight batches that were not yet flushed to the backend).

Frequently asked questions


Now that you understand CI log streaming, explore related build infrastructure and logging topics: