Skip to content

Ship your logs ​

Your applications already produce thousands of log lines a day across dozens of services. Almost none of them matter, and the few that do are buried. Horus takes the whole stream, compacts it, and tells you in plain language about the handful of things worth a look.

Log Intelligence lives under Logs in the sidebar. Adding a source requires the admin role.


How it works ​

Horus never stores your raw log lines. It runs a three-tier pipeline we call the intelligence hierarchy:

TierWhatCost
0. CompactionEvery line is normalized to a template (db timeout after <NUM>ms) and counted per service. Thousands of lines become tens of patterns.Free, deterministic
1. ClassifierA fast, cheap model looks at each new pattern once, labels it (security, availability, performance, data integrity, config, noise) and decides whether to escalate.Fractions of a cent
2. AnalystA strong model examines each escalated pattern with its examples and recent context, decides whether to alert, and writes what happened, why it matters and how to fix it.A few cents per escalation

Alerts are findings, listed under Logs → Alerts in their own section so they never mix with vulnerabilities, and they reach the in-app bell and any Slack, Teams or email integration you have configured (see Set up alerts). Marking one as a false positive silences that pattern for good; resolving it re-arms the pattern so you hear about it again only if it comes back.


Step 1: Add a source in Horus ​

  1. Open Logs → Add source.
  2. Name it after the shipper or environment (prod-k8s, web-tier).
  3. Pick the minimum severity to analyse. WARN is the sensible default: lower levels are still counted, but only warnings and above reach the models.
  4. Horus shows an ingest key (lgk_…) and ready-to-paste configs. The key is shown once, copy it now.

Step 2: Point a shipper at Horus ​

The endpoint speaks standard OTLP/HTTP logs (protobuf or JSON, gzip welcome), so any OpenTelemetry-compatible shipper works with no Horus-specific software.

POST https://<your-horus-api>/api/logs/v1/logs
Authorization: Bearer lgk_...

Vector ​

Vector's otlp codec only encodes events that already have the OTLP shape, so a small remap builds resourceLogs from the raw event (tested with Vector 0.58):

yaml
sources:
  app:
    type: docker_logs        # or: journald, file, kubernetes_logs
transforms:
  to_otlp:                   # Vector's otlp codec needs OTLP-shaped events
    type: remap
    inputs: [app]
    source: |
      svc = if exists(.container_name) { to_string(.container_name) ?? "app" }
            else if exists(.SYSLOG_IDENTIFIER) { to_string(.SYSLOG_IDENTIFIER) ?? "host" }
            else { "app" }
      prio = if exists(.PRIORITY) { to_int(.PRIORITY) ?? 6 } else { 6 }
      sev = if prio <= 2 { 21 } else if prio == 3 { 17 } else if prio == 4 { 13 } else { 9 }
      ts = to_unix_timestamp(.timestamp, unit: "nanoseconds") ?? to_unix_timestamp(now(), unit: "nanoseconds")
      . = { "resourceLogs": [{
        "resource": { "attributes": [{ "key": "service.name", "value": { "stringValue": svc } }] },
        "scopeLogs": [{ "logRecords": [{
          "timeUnixNano": to_string(ts), "severityNumber": sev,
          "body": { "stringValue": to_string(.message) ?? "" }
        }]}]
      }]}
sinks:
  horus:
    type: opentelemetry
    inputs: [to_otlp]
    protocol:
      type: http
      uri: https://<your-horus-api>/api/logs/v1/logs
      method: post
      encoding:
        codec: otlp
      auth:
        strategy: bearer
        token: lgk_...
      batch:
        max_events: 500
        timeout_secs: 5

Fluent Bit ​

yaml
pipeline:
  inputs:
    - name: tail
      path: /var/log/containers/*.log
      tag: app
  outputs:
    - name: opentelemetry
      match: '*'
      host: <your-horus-api>
      port: 443
      tls: on
      logs_uri: /api/logs/v1/logs
      header: Authorization Bearer lgk_...
      compress: gzip

OpenTelemetry Collector ​

yaml
exporters:
  otlphttp/horus:
    logs_endpoint: https://<your-horus-api>/api/logs/v1/logs
    headers:
      Authorization: Bearer lgk_...
    compression: gzip
service:
  pipelines:
    logs:
      receivers: [filelog]
      exporters: [otlphttp/horus]

Set the service.name resource attribute in your shipper when you can. Horus groups patterns by service; without it, it falls back to the container, deployment or host name.


Step 3: Read the results ​

  • Logs → Alerts: what the analyst decided you should know. Each one has a title, what happened, why it matters and numbered steps to fix or investigate. Mark in progress, resolved, or false positive; link it to an incident like any other finding.
  • Patterns: every distinct template with its count, severity, category and status. Mute anything you never want analysed again.
  • Sources: your shippers, their thresholds and when they last reported.

Tuning cost and noise ​

  • Batch bigger, not faster. Each source accepts up to 600 batches per minute and 4 MB per batch. A 5-second batch window is plenty.
  • Raise the threshold on chatty sources (ERROR instead of WARN) to keep the classifier focused.
  • Mute liberally. A muted pattern costs nothing forever.
  • The model tiers are configurable for self-hosted deployments: LOG_CLASSIFIER_MODEL (default openai/gpt-5-nano) and LOG_ANALYST_MODEL (defaults to your main model). LOG_ANALYST_MAX_PER_RUN caps analyst calls per organization per run.

Log lines are treated as untrusted input: they are delimited as data in every prompt and pseudonymized (IPs, emails, hostnames) before leaving Horus, the same privacy mode the rest of the platform uses. Secrets that leak into logs (API keys, bearer tokens, JWTs, PASSWORD=... pairs, bot tokens) are scrubbed to <SECRET> at ingest, so they are never stored in a sample nor shown to a model. Crash dumps that print a process environment are exactly how this happens in practice.

Released under the MIT License.