Skip to main content

Analytics events

Every event is an Otoroshi AnalyticEvent, so it flows through any data exporter — Elastic, Kafka, a webhook, a file — with no extra wiring.

If you only wire one of them up, wire up the first.

All of them carry Otoroshi's envelope: @serviceId and @service are the id and name of the route the request was on, and @env is Otoroshi's app.env. The route is -- only where there is none: the incoming request validator runs before routing.

CloudApimSecurityEvent​

The normalised one. Every component of the fabric — the threat response, the honeypot, fail2ban, a challenge, a ban — emits this shape rather than its own, so a SIEM needs one parser and one set of dashboards.

The envelope is ECS-shaped, so event.category, event.action, event.outcome and source.ip land where an Elastic pipeline already looks for them.

{
"@type": "CloudApimSecurityEvent",
"@timestamp": 1757340000000,
"@product": "otoroshi",
"event": {
"kind": "alert", "module": "cloud-apim.security-suite",
"dataset": "cloud-apim.threat", "category": "threat",
"action": "tarpit", "outcome": "observed", "severity": 2
},
"source": { "ip": "203.0.113.9", "apikey": null, "user": null },
"threat": {
"score": 75,
"tags": ["reputation:firehol1", "waf:match", "bot:seo"],
"signals": [
{ "source": "reputation.feed", "kind": "reputation", "weight": 50,
"tag": "reputation:firehol1", "confidence": 1.0, "effective_weight": 50 }
]
},
"otoroshi": { "route_id": "route_...", "route_name": "public-api", "node": "otoroshi-node-1" },
"incident": { "id": "...", "count": 34 },
"message": "score 75 reached tier 70 (tarpit)",
"decision": { "action": "tarpit", "score": 75, "tier": 1, "dry_run": true, "enforced": false, "held_ms": 0 }
}
FieldMeaning
event.categoryWhich component decided: threat, honeypot, fail2ban, challenge, ban, traffic, api, upload, login, objects, leakage or sensitive_data
event.actionallow, log, tarpit, challenge, deny, ban, or mask for a leaking response replaced by a neutral error and for sensitive values masked in place. A response the sensitive data guard refuses is a deny
event.outcomeblocked when it was enforced, observed in dry run
event.severity1 to 4, derived from the score
threat.signals[]Full attribution — which detector contributed what, and how sure it was
incident.countHow many events this identity has already produced in the window
decision.enforcedThe unambiguous answer to "did this actually do anything"
decision.held_msHow long the request was held before the action took effect: a tarpit, or a slow refusal. 0 when it was not
The pair to watch while tuning

outcome: "observed" with a non-log action is the dry-run equivalent of "this would have been enforced". Counting those over a week of real traffic is what tells you whether your tiers are set correctly — the same discipline as blocking: false on the WAF trail event below.

CloudApimWafTrailEvent​

Emitted once per request where at least one WAF rule matched. The main signal.

{
"@type": "CloudApimWafTrailEvent",
"@timestamp": 1757340000000,
"blocking": true,
"block": { "status": 403, "msg": "SQL Injection Attack Detected via libinjection" },
"events": [
{ "rule_id": 942100, "msg": "SQL Injection Attack Detected via libinjection", "phase": 2 }
],
"route": { "id": "route_...", "name": "public-api" },
"request": { "method": "GET", "uri": "/search?q=...", "headers": {} }
}
FieldMeaning
blockingWhether the configuration is in blocking mode
blockNon-null when the ruleset reached a deny decision
events[]Every rule that matched
truncatedThe body was longer than the inspection limit, so only its beginning was seen
oversize_rejectedThe request was refused for being past the limit, without being inspected
rejected_reasonWhy a body was refused before any rule read it: decompressed_size, compression_ratio, undecodable_encoding, corrupt_body

blocking: false with a non-null block is the "would have been denied" case — the number to track while tuning.

CloudApimWafReputationEvent​

Emitted whenever a reputation lookup produced at least one hit, in monitoring mode too.

{
"@type": "CloudApimWafReputationEvent",
"blocked": true,
"mode": "block",
"verdict": {
"ip": "203.0.113.7",
"score": 95,
"blocking": true,
"tags": ["feed:firehol-l1", "asn:aws"],
"hits": [
{ "kind": "feed", "source_id": "threat-feed_...", "source_name": "FireHOL level 1",
"weight": 80, "blocking": true }
]
}
}

verdict.tags is the field to group on: it carries the feed tags and CrowdSec origin, so a single query answers "what is each source actually catching".

CloudApimWafAuditEvent​

Emitted by SecLang auditlog actions. Rule-level audit output, considerably more verbose than the trail event — useful when debugging a specific rule, noisy as a permanent stream.

{
"@type": "CloudApimWafAuditEvent",
"@serviceId": "route_...",
"@service": "public-api",
"@env": "prod",
"rule_id": 942100,
"phase": 2,
"msg": "SQL Injection Attack Detected via libinjection",
"logdata": []
}

CloudApimWafTuningAudit​

An Otoroshi audit event, emitted when the tuning assistant writes an exclusion. It records the decision and the evidence together.

{
"@type": "CloudApimWafTuningAudit",
"audit": "WAF_EXCLUSION_APPLIED",
"user": { "name": "Ada", "email": "ada@example.com" },
"config": "waf-config_x",
"rule_id": 942100,
"target": "ARGS:comment",
"kind": "update_target",
"seclang": "SecRuleUpdateTargetById 942100 \"!ARGS:comment\"",
"reason": "the comment field legitimately contains SQL snippets",
"ruleset": { "id": "waf-ruleset_y", "created": true },
"preview": { "reproduced": true, "effective": true, "regressions": [], "collateral_rules": [] },
"forced": false
}

preview is the measured consequence at the moment of the decision — what was verified, and what was knowingly given up. forced is true only when the operator overrode a warning.

CloudApimWafLearningAudit​

Emitted when exclusions are applied from a learning run. It carries the window the decision was taken on, not only the change.

{
"@type": "CloudApimWafLearningAudit",
"audit": "WAF_LEARNING_APPLIED",
"user": { "name": "Ada", "email": "ada@example.com" },
"config": "waf-config_x",
"applied": ["942100|ARGS:comment|/api/posts"],
"run": { "requests": 41203, "matched": 380, "would_block": 96, "duration_millis": 604800000 },
"impact": { "sampled": 200, "resolved": 187, "residual": 13 },
"paranoia": { "current": 1, "recommended": null },
"threshold": { "current": 5, "recommended": 15 }
}

CloudApimWafSecurityAudit​

Emitted for every operator action taken on the Bans & incidents console — a ban, an extension, an unban, an allowlist entry, or an incident moved to a new state.

{
"@type": "CloudApimWafSecurityAudit",
"audit": "SECURITY_OPERATOR_ACTION",
"user": { "name": "Ada", "email": "ada@example.com" },
"action": "allowlist",
"ref": "apikey:partner-ci",
"detail": { "reason": "nightly integration suite", "until": null, "released": true }
}

action is one of ban, ban-refused, extend, unban, unban-all, allowlist, allowlist-remove, incident-acknowledged, incident-resolved or incident-open. A refused ban is audited too: knowing that someone tried to ban a caller the allowlist protects is worth as much as knowing they succeeded.

CloudApimSecurityAlert​

One per alert an alert rule sends, once for the whole cluster, whatever its channel: route it with a data exporter to anything Otoroshi reaches, a mailer included.

{
"@type": "CloudApimSecurityAlert",
"event": { "kind": "alert", "dataset": "cloud-apim.alert", "category": "incident", "severity": 3 },
"alert": {
"rule": { "id": "alert-rule_1f3a...", "name": "Enforced attacks" },
"trigger": "incident",
"key": "incident:ip:203.0.113.7",
"level": "high",
"title": "Incident on ip 203.0.113.7: score 90",
"summary": "waf block on /login",
"identity": { "kind": "ip", "value": "203.0.113.7" },
"facts": { "Score": "90", "Decisions": "12, 12 enforced" },
"details": { "...": "the incident, the ban, or the burst count" },
"link": "https://otoroshi.example.com/bo/dashboard/extensions/cloud-apim/waf/security"
}
}

CloudApimSecurityOcsf​

Emitted only when security.events.ocsf is on: every CloudApimSecurityEvent and every alert again, as an OCSF 1.3 Detection Finding (class 2004), for Security Lake, Sentinel and the other SIEMs that read OCSF. A decision says what was done in OCSF's terms: action_id 2 and disposition_id 2 when it was enforced, 3 and 15 when it was only observed, 4 and 11 for a masked response. The fabric's own fields that OCSF has no place for are under unmapped. The ECS-shaped events stay what the console reads; this is an additional stream.

Suggested exporters​

GoalFilter on
The one dashboard to build first@type: CloudApimSecurityEvent, group by event.category and event.action
Alerting without a pager storm@type: CloudApimSecurityAlert, or an alert rule straight to the channel
A SIEM that reads OCSF@type: CloudApimSecurityOcsf, with security.events.ocsf on
Is the policy ready to arm?@type: CloudApimSecurityEvent, outcome: observed and event.action != log
Attack dashboard@type: CloudApimWafTrailEvent, group by events[].rule_id
Tuning backlogblocking: false and block != null, group by rule id
Reputation coverage@type: CloudApimWafReputationEvent, group by verdict.tags
Alertingblocked: true or blocking: true with block != null, over a burst window
Who changed what during the incident@type: CloudApimWafSecurityAudit, group by user.email