SOC log architecture and taxonomy form the operational backbone of automated security operations. Without a rigorously staged ingestion pipeline and a consistent event classification framework, correlation degrades into probabilistic guesswork and automation fractures the moment a vendor renames a field. The discipline covered here turns raw, heterogeneous telemetry into a deterministic, schema-validated event stream that downstream systems can trust — the same contract that the alert correlation and rule engines consume when they join identity, endpoint, and network events into incidents. This page maps the architecture end to end: how events enter, how they are classified, how fields are normalized, how failures are contained, how the pipeline scales, and how every stage is made audit-ready.
Stage-Gate Architecture
A resilient SOC log architecture behaves as a directed transformation pipeline, not a monolithic store. Each stage is a gate: an event either satisfies the stage’s contract and advances, or it is diverted to a dead-letter path with a typed error code. Treating every boundary as a gate is what makes the system deterministic — the state of an event is always knowable from the last gate it cleared.
The canonical stages are:
- Collection. Agents, API pollers, syslog receivers, and cloud-native streams (CloudWatch, Azure Monitor, GCP Logging) land raw bytes onto a durable buffer — typically a Kafka topic or a managed queue. Collection never parses; it only frames, timestamps the receive event, and tags provenance.
- Parsing. Format-aware parsers convert raw frames into dictionaries. This is where the broader log ingestion and parsing workflows attach: delimiter detection, regex extraction for unstructured lines, and structured decoders for JSON and key-value payloads.
- Normalization. Parsed dictionaries are coerced into a single canonical schema — field aliasing, type coercion, and timestamp standardization to UTC.
- Enrichment. Asset criticality, identity context, geolocation, and threat indicators are attached. This stage is read-mostly and must tolerate enrichment-source outages without blocking the pipeline.
- Validation gate. A strict schema check rejects non-conforming events into a dead-letter queue (DLQ) before they reach correlation. This is the single most important gate for analytic integrity.
- Routing. Validated events fan out to the correlation engine, hot search storage, and cold compliance storage on independent paths so a slow consumer never stalls a fast one.
Each stage must be idempotent, observable, and version-controlled. Platform teams deploy collectors, brokers, and stream processors as configuration-as-code, while security engineers own the schema contract enforced at the validation gate. Because every gate emits structured metrics — events in, events out, events diverted — pipeline observability is a first-class output rather than an afterthought.
Taxonomy & Classification
Taxonomy is the semantic layer that translates vendor-specific telemetry into a unified security ontology. A workable taxonomy classifies every event along three orthogonal axes: source class, event category, and delivery format. Confusing these axes is the most common cause of brittle routing logic.
By source class — the system of origin determines which parser and enrichment set applies:
- Network and infrastructure devices (firewalls, routers, proxies) — predominantly Syslog RFC standards traffic governed by RFC 5424 and RFC 3164.
- Cloud and SaaS control planes — structured JSON from audit APIs, normalized through JSON event normalization.
- Endpoint and EDR agents — high-cardinality process, file, and registry telemetry.
- Batch and compliance exports — delimited records handled by CSV ingestion patterns, where delimiter and quote ambiguity dominate failure modes.
- External intelligence — indicator and TTP records reconciled through threat intel feed mapping.
By event category — aligned to the ECS event.category allowed values (authentication, network, process, file, iam, malware, intrusion_detection, configuration) so that correlation rules join on a closed vocabulary rather than free text. Categories map cleanly to adversary behavior: an authentication failure burst followed by a process creation chain is the raw material for detecting Valid Accounts (T1078) escalating into Command and Scripting Interpreter (T1059) activity.
By delivery format — syslog, JSON, key-value, and delimited text each demand a distinct parser strategy and produce distinct failure classes. Holding format orthogonal to source class is what lets a single JSON normalizer serve both a cloud audit log and a modern firewall that happens to emit structured output.
The decisive design rule is that taxonomy is a version-controlled contract, not documentation. Field names, category enumerations, and severity bands live in the same repository as the parsers, are reviewed on pull request, and are released as immutable schema versions. When a vendor update changes a field, the contract changes through code review — not silently in production.
Schema & Field Normalization
Normalization collapses every source dialect into one canonical schema. This site standardizes on the Elastic Common Schema (ECS), with OSSEM as a cross-reference for detection coverage. A minimal mandatory field set anchors every normalized event:
| Canonical field | ECS name | Type | Enforcement |
|---|---|---|---|
| Event ID | event.id |
string (non-empty) | reject if empty |
| Timestamp | @timestamp |
RFC 3339 UTC | coerce, never drop tz |
| Category | event.category |
enum | reject if outside vocabulary |
| Severity | event.severity |
int 0–7 | clamp to syslog band |
| Source address | source.ip |
IPv4/IPv6 | validate, null-safe |
| Destination address | destination.ip |
IPv4/IPv6 | validate, null-safe |
| Provenance | observer.product |
string | tag at collection |
Three normalization rules are non-negotiable. First, timestamp standardization to UTC — every parser converts to a timezone-aware UTC instant at the normalization boundary; naive local timestamps are the single largest source of correlation timeline drift across hybrid estates. Second, type coercion with bounded failure — a severity that arrives as "high", 5, or 5.0 must resolve to one integer band, and an unresolvable value is a typed error, not a silent default. Third, null-safe aliasing — src_ip, source.address, and sourceIPAddress all alias to source.ip, and a missing source is represented explicitly rather than as an empty string that poisons downstream joins.
Field-level validation belongs at the gate, not scattered through business logic. The deeper mechanics of contract enforcement — required-field policies, conditional schemas, and version negotiation — are covered in schema validation pipelines, which the validation gate invokes for every event.
Resilience & Failure Modes
A pipeline that cannot fail safely cannot be automated against. The architecture treats every failure as a routable event with a typed code rather than an exception that crashes a worker. Error codes follow the ERR_CATEGORY_NNN convention used across this site:
| Code | Meaning | Action |
|---|---|---|
ERR_PARSE_001 |
Unparseable frame / malformed delimiter | Route to DLQ, sample for parser fix |
ERR_SCHEMA_002 |
Field fails schema validation | Route to DLQ, alert if rate exceeds baseline |
ERR_NORM_003 |
Timestamp/type coercion failed | Route to DLQ, quarantine source for review |
ERR_ENRICH_004 |
Enrichment source unavailable | Pass through un-enriched, flag enrichment.partial |
ERR_BACKPRESSURE_005 |
Downstream buffer saturated | Apply backpressure, shed lowest-severity first |
Four resilience patterns make those codes actionable. Dead-letter routing isolates poison events so one malformed batch cannot stall the stream; DLQ contents are replayable once a parser is patched, preserving the at-least-once guarantee. Circuit breakers wrap every external enrichment and storage call so a slow threat-intel API trips open and degrades gracefully — events flow through marked enrichment.partial rather than queuing to exhaustion. Backpressure propagates from sink to source via bounded queues and semaphores, so the system sheds or slows deterministically instead of running out of memory. Idempotent routing keys every event by event.id plus content hash so a replayed DLQ batch never double-counts in correlation.
A disciplined error taxonomy is itself a product: it feeds the error categorization frameworks that turn raw failure events into trends, so an emerging ERR_SCHEMA_002 spike surfaces a vendor schema change before it silently erodes detection coverage.
Performance & Scale
SOC pipelines are measured in sustained events per second under bursty, adversarial load. The throughput target for a single normalization worker pool on commodity hardware is 20k–50k events/sec; aggregate capacity scales horizontally by partitioning the collection buffer and running stateless parser/normalizer workers per partition.
Three patterns keep throughput high without exhausting memory:
- Memory-safe streaming. Never materialize a full batch in memory. Parsers consume from the buffer as an async iterator, yielding normalized events downstream so resident set size stays flat regardless of stream volume. A bounded
asyncio.Queue(depth 10k–50k) caps in-flight events. - Async concurrency with admission control. A semaphore bounds concurrent in-flight events (500–2000 is typical) so a latency spike in enrichment cannot fan out into unbounded task creation. Concurrency is for hiding I/O latency, not for parallelizing CPU work — CPU-bound parsing of large payloads belongs in a process pool.
- Batched sinks. Correlation and storage writes are micro-batched (250–1000 events or a 200 ms flush window, whichever first) to amortize network round-trips while keeping tail latency inside a one-second budget.
Capacity planning starts from the slowest gate. Enrichment is almost always the bottleneck because it makes network calls; sizing the enrichment worker pool and its cache hit rate, not raw parser speed, sets the pipeline’s ceiling.
Production Implementation
The following processor implements the validation gate, DLQ routing, and admission control described above. It is async, fully typed, and validated with Pydantic v2. pydantic and aiohttp are third-party dependencies (pip install pydantic aiohttp); the code uses the Pydantic v2 @field_validator API.
import asyncio
import logging
from datetime import datetime, timezone
from typing import Any, AsyncIterator, Dict, Optional
from pydantic import BaseModel, Field, ValidationError, field_validator
logger = logging.getLogger("soc.log_processor")
VALID_CATEGORIES = {
"authentication", "network", "process", "file",
"iam", "malware", "intrusion_detection", "configuration",
}
class NormalizedEvent(BaseModel):
"""Canonical, ECS-aligned event enforced at the validation gate."""
event_id: str = Field(..., min_length=1)
timestamp: datetime
source_ip: Optional[str] = None
destination_ip: Optional[str] = None
event_category: str
severity: int = Field(ge=0, le=7)
enrichment_partial: bool = False
@field_validator("timestamp", mode="before")
@classmethod
def coerce_timestamp(cls, v: Any) -> datetime:
# ERR_NORM_003 surfaces here when coercion is impossible.
if isinstance(v, str):
return datetime.fromisoformat(v.replace("Z", "+00:00")).astimezone(timezone.utc)
if isinstance(v, (int, float)):
return datetime.fromtimestamp(v, tz=timezone.utc)
raise ValueError("unsupported timestamp format")
@field_validator("severity", mode="before")
@classmethod
def clamp_severity(cls, v: Any) -> int:
return max(0, min(7, int(v)))
@field_validator("event_category")
@classmethod
def check_category(cls, v: str) -> str:
if v not in VALID_CATEGORIES:
raise ValueError(f"category '{v}' outside controlled vocabulary")
return v
# Source-dialect aliases collapse to canonical field names at normalization.
ALIASES = {
"event_id": ("event.id", "id", "event_id"),
"timestamp": ("@timestamp", "timestamp", "eventTime"),
"source_ip": ("source.ip", "src_ip", "sourceIPAddress"),
"destination_ip": ("destination.ip", "dst_ip", "destinationIPAddress"),
"event_category": ("event.category", "category", "type"),
"severity": ("event.severity", "severity", "level"),
}
def _first(raw: Dict[str, Any], keys: tuple) -> Any:
for k in keys:
if k in raw and raw[k] is not None:
return raw[k]
return None
class AsyncLogProcessor:
"""Validation gate with admission control and dead-letter routing."""
def __init__(self, max_inflight: int = 1000, schema_version: str = "v2.1") -> None:
self.schema_version = schema_version
self._semaphore = asyncio.Semaphore(max_inflight)
self._dlq: asyncio.Queue = asyncio.Queue()
async def process_event(self, raw: Dict[str, Any]) -> Optional[NormalizedEvent]:
async with self._semaphore:
try:
event = NormalizedEvent(
event_id=_first(raw, ALIASES["event_id"]) or "",
timestamp=_first(raw, ALIASES["timestamp"]),
source_ip=_first(raw, ALIASES["source_ip"]),
destination_ip=_first(raw, ALIASES["destination_ip"]),
event_category=_first(raw, ALIASES["event_category"]) or "",
severity=_first(raw, ALIASES["severity"]) or 0,
)
logger.debug("normalized event_id=%s", event.event_id)
return event
except ValidationError as exc:
logger.warning("ERR_SCHEMA_002 event rejected: %s", exc.errors())
await self._dlq.put({"code": "ERR_SCHEMA_002", "payload": raw})
return None
except Exception: # pragma: no cover - defensive boundary
logger.exception("ERR_PARSE_001 unexpected processing failure")
await self._dlq.put({"code": "ERR_PARSE_001", "payload": raw})
return None
async def run(self, stream: AsyncIterator[Dict[str, Any]]) -> AsyncIterator[NormalizedEvent]:
async for raw in stream:
event = await self.process_event(raw)
if event is not None:
yield event
async def drain_dlq(self) -> None:
while not self._dlq.empty():
failed = await self._dlq.get()
logger.critical("DLQ %s: %s", failed["code"], failed["payload"])
await asyncio.sleep(0) # cooperative yield
async def _sample_stream() -> AsyncIterator[Dict[str, Any]]:
for raw in [
{"id": "evt-001", "@timestamp": "2026-06-15T10:30:00Z", "src_ip": "10.0.0.5",
"dst_ip": "192.168.1.10", "event.category": "authentication", "severity": 3},
{"id": "evt-002", "@timestamp": "invalid", "src_ip": "10.0.0.6",
"event.category": "network", "severity": 5},
]:
yield raw
async def main() -> None:
processor = AsyncLogProcessor(max_inflight=500)
async for event in processor.run(_sample_stream()):
logger.info("forwarding %s to correlation", event.event_id)
await processor.drain_dlq()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())
This implementation enforces type coercion, isolates failures behind typed DLQ codes, and bounds concurrency with a semaphore so an enrichment latency spike cannot exhaust memory. Security engineers should layer TLS mutual authentication and signature verification at the transport boundary before events ever reach this gate.
Strategic Alignment
A log architecture is only as durable as the team workflow that maintains it. Three roles share ownership, and the boundary between them is the schema contract:
- SOC analysts consume the normalized output. They author and tune detections, and their feedback on false positives drives schema and enrichment changes. They do not edit parsers directly; they file contract changes.
- Detection / security engineers own the taxonomy contract — the canonical schema, category vocabulary, and severity bands — and the correlation logic that depends on it. They map detections to adversary behavior through MITRE ATT&CK integration so every alert carries a tactic and technique reference.
- Platform / DevOps engineers own the runtime: collectors, brokers, worker pools, and autoscaling.
Configuration-as-code is the connective tissue. Parsers, schema definitions, enrichment maps, and routing rules live in Git. Every change runs through CI: synthetic event fixtures are replayed against the new parser, the canonical schema is validated, and a golden-output diff catches unintended normalization drift before merge. Deployment is GitOps — an immutable artifact is promoted from staging to production, and a faulty schema version rolls back by reverting a commit, never by hot-editing a box. This is what lets a SOC change detection logic daily without destabilizing the telemetry plane underneath it.
Compliance & Audit Considerations
Log architecture decisions are also audit decisions, because the pipeline is the system of record for security evidence. Four obligations shape the design:
- NIST SP 800-92 (Guide to Computer Security Log Management) anchors the lifecycle: standardized categorization, controlled retention, and protected transport. The taxonomy contract is the direct implementation of its categorization guidance.
- NIST SP 800-53 Rev. 5 audit controls map onto specific gates: AU-3 (content of audit records) maps to the mandatory field set, AU-4 (storage capacity) to backpressure and capacity planning, AU-6 (review and reporting) to the correlation outputs, and AU-9 (protection of audit information) to the DLQ and immutable cold storage.
- PCI-DSS v4.0 Requirement 10 mandates that audit trails capture user identity, event type, timestamp, and outcome for in-scope systems, with Requirement 10.4 requiring daily review — both satisfied by enforcing
event.id,@timestamp, identity enrichment, andevent.categoryat the validation gate. - ISO/IEC 27001:2022 Annex A control 8.15 (Logging) requires that event logs be produced, kept, and protected; the version-controlled schema contract plus immutable storage provides the demonstrable control evidence auditors expect.
The practical takeaway: because every gate emits typed metrics and every rejected event lands in a replayable DLQ, the architecture produces audit evidence as a byproduct of normal operation rather than as a separate compliance project.
FAQ
What is the difference between log architecture and log taxonomy?
Architecture is the staged runtime that moves an event from collection through validation to correlation and storage. Taxonomy is the semantic contract — source class, event category, and format — that classifies each event so the architecture can route and normalize it deterministically. Architecture is the pipes; taxonomy is the schema flowing through them.
Why standardize on ECS instead of a custom schema?
The Elastic Common Schema gives a closed, documented field vocabulary and a controlled event.category enumeration, so correlation rules join on stable names rather than vendor strings. A custom schema can work, but it shifts the cost of maintaining and documenting that vocabulary onto your team and breaks interoperability with detection content authored against ECS or OSSEM.
Where should schema validation happen in the pipeline?
At a single explicit gate immediately after enrichment and before correlation. Validating at one boundary — rather than scattering checks through business logic — means an event's state is always knowable from the last gate it cleared, and every rejection produces one typed error code (ERR_SCHEMA_002) routed to the dead-letter queue.
How do I keep correlation timelines consistent across hybrid sources?
Standardize every timestamp to a timezone-aware UTC instant at the normalization boundary, never downstream. Naive local timestamps are the largest source of timeline drift across on-prem, cloud, and SaaS sources. Pair UTC coercion with a provenance tag so you can reconstruct the original source offset if an investigation requires it.
What happens to events that fail validation?
They are routed to a dead-letter queue with a typed code (ERR_PARSE_001, ERR_SCHEMA_002, or ERR_NORM_003) rather than dropped or crashing a worker. DLQ contents are replayable once the underlying parser or schema is patched, which preserves the at-least-once delivery guarantee and gives the error-categorization layer the data it needs to spot an emerging vendor schema change.
How does this architecture stay audit-ready?
Every gate emits typed metrics and every rejected event lands in a replayable DLQ, so audit evidence is a byproduct of normal operation. The mandatory field set satisfies NIST SP 800-53 AU-3 and PCI-DSS Requirement 10 content rules, immutable cold storage satisfies AU-9 protection, and the version-controlled taxonomy contract provides the ISO/IEC 27001:2022 control 8.15 evidence auditors request.