Skip to content
HALKON telemetry ingest & retention
docs build 2026-09-08 | agent 4.2.1 | HKQL v3 | your clock —

Documentation

From nothing to a first query

Ten minutes on one host, assuming a shell and outbound TCP 443. Nothing to compile, no SDK to add to your application, no sidecar unless you want one.

1 · Quick start

  1. Create an ingest key

    In the console, or through the management API with an existing admin key. A key is bound to one region and cannot be moved.

    shellmanagement API
    curl -sS -X POST \
      -H 'Authorization: Bearer hk_ak_REPLACE_ME' \
      -H 'Content-Type: application/json' \
      --data '{"name":"prod-hosts","scope":{"signals":["logs","metrics"],"source_prefix":"web-"}}' \
      https://api.halkon.sbs/v1/keys
    
    # response, shown exactly once:
    # {"id":"k-7f21","key":"hk_ik_....","region":"eu-north-1","created":"2026-09-08T11:02:44Z"}
  2. Install the agent

    A static binary plus a unit file. Verify the checksum published with the release before installing; the installer does that for you and refuses on mismatch.

    shellDebian or RPM host
    # 1. put the key somewhere only the agent can read
    sudo install -d -m 0750 -o hkagent -g hkagent /etc/hkagent
    printf '%s' "$LP_INGEST_KEY" | sudo tee /etc/hkagent/ingest.key >/dev/null
    sudo chmod 0400 /etc/hkagent/ingest.key
    sudo chown hkagent:hkagent /etc/hkagent/ingest.key
    
    # 2. minimal config
    sudo tee /etc/hkagent/agent.yaml >/dev/null <<'YAML'
    agent:
      key_file: /etc/hkagent/ingest.key
      source: "${HOSTNAME}"
    egress:
      endpoint: ingest.halkon.sbs
    collect:
      host_metrics:
        enabled: true
        interval: 15s
      files:
        - path: /var/log/app/*.log
          format: json
    YAML
    
    # 3. start
    sudo systemctl enable --now hkagent
    systemctl status hkagent --no-pager
  3. Or skip the agent entirely

    If you already run an OpenTelemetry Collector, add an exporter and you are done. Nothing else on the host changes.

    otel-collector.yamlexporter fragment
    exporters:
      otlphttp/halkon:
        endpoint: https://ingest.halkon.sbs:4318
        compression: zstd
        headers:
          Authorization: "Bearer ${env:LP_INGEST_KEY}"
        sending_queue:
          enabled: true
          num_consumers: 4
          queue_size: 5000
          storage: file_storage/halkon   # survive a restart
        retry_on_failure:
          enabled: true
          initial_interval: 1s
          max_interval: 60s
          max_elapsed_time: 0s              # never give up, let the queue hold
    
    processors:
      batch:
        send_batch_size: 4000
        send_batch_max_size: 8000
        timeout: 2s
    
    service:
      pipelines:
        logs:
          receivers: [otlp, filelog]
          processors: [batch]
          exporters: [otlphttp/halkon]
        metrics:
          receivers: [otlp, hostmetrics]
          processors: [batch]
          exporters: [otlphttp/halkon]
  4. Confirm data is being accepted

    Three independent checks, in increasing order of confidence.

    shelllocal, then remote
    # a) what the agent thinks
    curl -s http://localhost:9411/metrics | grep -E '^hkagent_(state|batches_accepted_total|buffer_bytes)'
    
    # b) a hand-made event, acknowledged or refused on the spot
    printf '{"ts":"%s","severity":"INFO","service":"smoke","body":"hello from %s"}\n' \
      "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$(hostname)" \
      | curl -sS -i -X POST \
          -H "Authorization: Bearer $LP_INGEST_KEY" \
          -H 'Content-Type: application/x-ndjson' \
          --data-binary @- \
          https://ingest.halkon.sbs/v1/ingest/logs
    
    # c) read it back
    curl -sS -H "Authorization: Bearer $LP_QUERY_KEY" \
      -H 'Content-Type: application/json' \
      --data '{"query":"from logs | where ts > ago(5m) and service == \"smoke\" | limit 10"}' \
      https://query.halkon.sbs/v1/query

    Step (b) uses your host’s own clock. Right now that would be —, which must fall inside the −72 h / +5 min acceptance window.

  5. Run a real query

    Once host metrics are flowing there is something worth asking about.

    HKQLpaste into the console
    from metrics
    | where ts > ago(1h) and name == "system_memory_available_bytes"
    | summarize free_gb = min(value) / 1e9 by source, t = bin(ts, 5m)
    | sort by free_gb asc
    | limit 20
    | render line

2 · Keys and scopes

Three key types, three prefixes, no overlap in capability. A key is shown once at creation and stored only as a salted hash; if it is lost, rotate rather than recover.

PrefixTypeCanCannotMax per account
hk_ik_ingestWrite events in its signal set and source prefixRead anything, manage anything500
hk_qk_queryRun HKQL, read dashboards and alert historyWrite events, change rules, create keys100
hk_ak_adminManage keys, rules, retention policies and plansRead event content10

An admin key deliberately cannot read your telemetry. Separating “who can change the account” from “who can read the logs” is the point of having three types.

Scope fields

regionfixed at creation, immutable
signals[logs, metrics, traces] subset
source_prefixstring — rejects sources that do not match
rate_limitevents/s per key, optional
expires_attimestamp optional, max 400 d out
last_used_atread-only, 1 min resolution

Rotation without a window

  1. Create the new key with the same scope. Two keys per scope may be active at once.
  2. Roll it out to hosts. The agent reloads key_file on SIGHUP without dropping its stream.
  3. Watch last_used_at on the old key until it stops moving.
  4. Revoke the old key. Open streams using it close within 30 s; anything still holding it reconnects and gets 401, which is exactly the signal you want.
Keys in environment variables

Convenient and visible in /proc to every process the user can see. Prefer key_file with mode 0400. The agent never logs the key, never echoes it, and truncates it to its prefix in every diagnostic.


3 · Configuration examples

Kubernetes DaemonSet

daemonset.yamlone agent per node
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: hkagent
  namespace: observability
spec:
  selector:
    matchLabels: { app: hkagent }
  template:
    metadata:
      labels: { app: hkagent }
    spec:
      serviceAccountName: hkagent
      containers:
        - name: hkagent
          image: hkagent:4.2.1
          args: ["--config=/etc/hkagent/agent.yaml"]
          resources:
            requests: { cpu: 50m,  memory: 64Mi }
            limits:   { cpu: 500m, memory: 192Mi }
          env:
            - name: NODE_NAME
              valueFrom: { fieldRef: { fieldPath: spec.nodeName } }
          volumeMounts:
            - { name: config,  mountPath: /etc/hkagent,        readOnly: true }
            - { name: key,     mountPath: /etc/hkagent/secret, readOnly: true }
            - { name: buffer,  mountPath: /var/lib/hkagent }
            - { name: varlog,  mountPath: /var/log,            readOnly: true }
      volumes:
        - { name: config, configMap: { name: hkagent-config } }
        - { name: key,    secret:    { secretName: hkagent-ingest-key, defaultMode: 256 } }
        - { name: buffer, hostPath:  { path: /var/lib/hkagent, type: DirectoryOrCreate } }
        - { name: varlog, hostPath:  { path: /var/log } }

The buffer must be a hostPath or a persistent volume. On an emptyDir the buffer dies with the pod, which defeats the only mechanism that survives an outage.

Sending from a CI job

shellno agent, one POST at the end of the job
# Build metadata as a single structured event.
cat > /tmp/build-event.json <<JSON
{"ts":"$(date -u +%Y-%m-%dT%H:%M:%SZ)",
 "severity":"INFO",
 "service":"ci",
 "body":"build finished",
 "attrs":{"pipeline":"$CI_PIPELINE","status":"$JOB_STATUS",
          "duration_s":$SECONDS,"commit":"$GIT_SHA"}}
JSON

curl -sS --retry 5 --retry-connrefused --retry-delay 2 \
  -H "Authorization: Bearer $LP_INGEST_KEY" \
  -H 'Content-Type: application/x-ndjson' \
  --data-binary @/tmp/build-event.json \
  https://ingest.halkon.sbs/v1/ingest/logs

Retention policy

retention.yamlevaluated top to bottom, first match wins
policies:
  - name: audit-trail
    match: 'service == "payments" and attrs.audit == true'
    retain: 360d

  - name: errors
    match: 'severity in ("ERROR", "FATAL")'
    retain: 90d

  - name: host-metrics
    match: 'signal == "metrics" and name startswith "system_"'
    retain: 400d

  - name: default
    match: 'true'
    retain: 30d       # the included window, no extra charge

Lengthening a policy applies from the moment you save it and does not resurrect data already deleted. Shortening it takes effect at the next daily sweep, within 24 hours. Cost arithmetic: pricing → retention.


4 · Verifying ingest

Four counters answer almost every “is it working” question. They are exported by the agent on loopback and also shipped into your account, so you can read them either way.

CounterHealthyIf it is wrong
hkagent_state01 is transient. 2 or above means the link is gone — check egress, proxy, DNS, then status.
hkagent_batches_accepted_totalrisingFlat with a non-zero event rate means nothing is being acknowledged.
hkagent_batches_rejected_total0Rising means a 4xx. The reason is in the agent log, once per distinct reason per minute.
hkagent_buffer_bytes0Non-zero and rising is an outage in progress. Non-zero and falling is a drain.

5 · First query, annotated

HKQLevery line explained
from logs                                  # pick the source; exactly one per query
| where ts > ago(15m)                      # time first: this prunes whole blocks
| where service == "checkout-api"        # then an indexed attribute
| where body contains "timeout"          # raw-body filters go last, they are cost class C
| summarize n = count() by pod            # collapse to one row per pod
| sort by n desc                          # sorting is only legal after summarize or limit
| limit 20

Reorder those three where clauses and the query still returns the same rows, but it reads a different amount of data. HKQL does not reorder them for you — see what a query costs.


6 · Troubleshooting

SymptomMost likely causeCheck
Agent runs, nothing appears in queries wrong region The key is scoped to one region. A key from eu-north-1 at the na-east-1 host returns 403 with the required scope named in the body.
Everything rejected with 422, rule timestamp_past_window host clock Events older than 72 h are refused. Check NTP. The agent refuses to send at all if the clock jumps more than 5 min.
401 immediately after a successful rollout revoked key Some hosts still hold the old key. last_used_at on the revoked key tells you it is still in use somewhere.
Steady trickle of 429 per-key rate limit A key with rate_limit set, or too many single-event requests. Batch, or split into multiple keys.
429 only on new metric names series cardinality 5 M active series per account. Usually a label carrying a request id or a pod name in a metric that does not need it.
Buffer grows during business hours only egress saturation The uplink, not us. Compare hkagent_buffer_bytes against your own link utilisation.
Queries time out at exactly 60 s scan too wide Interactive timeout. Narrow the time predicate or move the cost class C filter later in the pipeline.
Events present but attributes missing truncation Look for lp.truncated=true. An attribute value over 8 KiB is cut and flagged, not dropped silently.

7 · Glossary

accepted byteA normalised, pre-compression byte that was durably written. The billing unit.
batchThe unit of acknowledgement. Lands whole or not at all.
blockTwo hours or 512 MiB of columnar storage. The unit the planner prunes.
cost classA, B or C — index, column or raw body. Decides scheduling, not price.
fingerprintStable identity of an alert instance, derived from rule id and group keys.
ingest pointThe regional endpoint that terminates agent connections.
instanceOne row of an alert rule’s result that matched the condition.
signallogs, metrics or traces. Keys and policies are scoped by signal.
sourceThe host or process identity attached to every event. Defaults to the hostname.
staleAn alert group that was reporting and stopped. A signal, not an error.