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:
| Tier | What | Cost |
|---|---|---|
| 0. Compaction | Every 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. Classifier | A 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. Analyst | A 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
- Open Logs → Add source.
- Name it after the shipper or environment (
prod-k8s,web-tier). - Pick the minimum severity to analyse.
WARNis the sensible default: lower levels are still counted, but only warnings and above reach the models. - 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):
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: 5Fluent Bit
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: gzipOpenTelemetry Collector
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 (
ERRORinstead ofWARN) 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(defaultopenai/gpt-5-nano) andLOG_ANALYST_MODEL(defaults to your main model).LOG_ANALYST_MAX_PER_RUNcaps 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.