Flight Log Parsing Pipelines
Modern flight operations rely on deterministic data flows to maintain regulatory compliance and optimise crew utilisation. At the core of that architecture sits the flight log parsing pipeline: a specialised extract-transform-load framework that turns raw telemetry, ACARS OOOI dumps, FMS exports, and maintenance-system outputs into structured, query-ready records. These pipelines are the bridge between disparate aircraft systems and the enterprise engines that judge legality. When engineered correctly, they eliminate manual reconciliation, enforce contractual and regulatory boundaries, and produce audit-ready trails. This work sits squarely within the broader flight data ingestion initiative, where data fidelity directly shapes every downstream compliance verdict — if the parsing boundary admits an ambiguous block time, every duty time validation rule engine built on top of it inherits the defect.
The Parsing Problem This Pipeline Solves
The scoped challenge is narrow but unforgiving: reduce many contradictory, out-of-order log records into exactly one canonical, UTC-stamped account of what each aircraft actually did, precise enough that a duty-limit calculation stands up to an FAA or EASA inspection months later. The arithmetic of block time is trivial. The difficulty is that the inputs to that arithmetic arrive late, duplicated, in conflicting formats, and stamped in local times that shift under daylight-saving transitions.
A single carrier routinely receives the same block-out event more than once — from an ACARS OOOI feed, from a crew-management export, and from a third-party tracking API — each with a slightly different timestamp and a different notion of what “departure” means. Raw logs also arrive in heterogeneous encodings: CSV, JSON, fixed-width ASCII, and proprietary binary streams. The parsing layer must normalise all of these into one event schema, resolve the disagreements through a defined precedence hierarchy, and only then admit a record to the ledger that the compliance engines read. A timestamp error that shifts a report time across a daylight-saving boundary can silently convert a compliant nine-hour flight duty period into an illegal one, or mask a genuine violation that an inspector will later find in the raw feed. Parsing is therefore a safety-critical subsystem, not plumbing.
Three properties make it hard. First, records are duplicated and must be deduplicated idempotently without ever collapsing two genuinely distinct legs. Second, the same clock instant carries different regulatory meaning depending on the crew member’s acclimatisation state and local report time. Third, a leg that looks clean in isolation can be illegal because of cumulative totals stretching back a year, so every parsed record must survive being replayed into a rolling window.
Figure: The parsing pipeline end to end — heterogeneous feeds converge, are normalised and validated, transformed into block, air and duty metrics, idempotently upserted on a composite key, mapped to crew, and either alerted for re-pairing or written to the scheduling ledger.
Schema and Data Structure Design
The parsing pipeline separates three concerns that operators routinely conflate: the raw inbound record as received, the canonical event derived from it, and the duty structures that compliance engines evaluate. Keeping the raw payload immutable and append-only is what makes every downstream value reproducible — a disputed block time can always be traced back to the exact bytes that produced it.
The core entities are the raw_log (the untouched inbound payload with its source and receipt instant); the canonical flight_event (one OOOI instant — out, off, on, or in — normalised to UTC with an originating-zone attribute); the flight_leg reconstructed from a matched set of four OOOI events; the duty_period that groups legs bounded by rest; and the crew_assignment that maps each leg to its operating crew. Every temporal column is stored in UTC with the originating IANA zone kept as metadata, so local report time is derived at evaluation, never stored ambiguously. A naive datetime must never cross the parsing boundary.
Field naming, unit standardisation, and the classification of positioning versus operating segments all follow the shared crew duty time taxonomy. Anchoring the schema to that taxonomy is what prevents the pipeline from misclassifying a positioning leg as an operating sector, or a standby callout as the start of a flight duty period — the two most common sources of false violations. Structural conformance of each inbound payload is enforced by the data schema validation rules that guard the ingestion edge, so malformed records are quarantined rather than allowed to corrupt the canonical store.
Figure: The parsing schema as an entity-relationship model — an immutable raw_log derives many flight_event OOOI instants, four of which reconstruct one flight_leg; legs are grouped into a duty_period and mapped to operating crew through crew_assignment.
Regulatory Mapping
Every schema decision above is driven by a specific regulatory provision, and the pipeline’s job is to compute the exact quantities those provisions constrain. The authoritative wording for US carriers lives in the FAA Part 117 rule schema; the European counterpart is governed by EASA FTL compliance frameworks.
- §117.3 — Definitions. Fixes what the pipeline must actually measure. Flight time runs from the moment the aircraft moves under its own power for the purpose of flight until it comes to rest after landing — the block-to-block span reconstructed from the OOOI
outandinevents. Flight duty period begins when a crew member reports and ends at block-in of the final leg. Getting these definitions into code precisely is why the schema stores discrete OOOI events rather than a single “flight time” number. - §117.11 — Flight time limitation. Caps flight time per flight duty period at 8 or 9 hours depending on report time. The pipeline’s derived block-to-block flight time per FDP is the quantity measured against this limit.
- §117.13 — Flight duty period: unaugmented operations. Sets the maximum FDP as a function of report time and number of flight segments (Table B). Because the ceiling depends on the segment count, the pipeline must reconstruct legs reliably before any FDP verdict is possible.
- §117.23 — Cumulative flight duty period and flight time limitations. Caps flight time at 100 hours in any 672 consecutive hours and 1,000 hours in any 365 consecutive days, and FDP at 60 hours in any 168 consecutive hours and 190 hours in any 672 hours. These rolling windows are the reason the schema stores leg-level rows, not daily totals.
- §117.25 — Rest period. Requires at least 10 consecutive hours of rest with a minimum 8-hour uninterrupted sleep opportunity before a flight duty period. The
duty_periodboundaries the pipeline derives are what the rest check is measured against.
The precedence rules for resolving conflicting timestamps, and the handling of standardised aeronautical formats, are worked through for navigation data in Parsing ARINC 424 Flight Logs with Python.
Python Implementation Walkthrough
Production implementations express the inbound contract as typed models so that a malformed payload fails at the boundary rather than deep inside the transform stage. Using pydantic for the canonical event guarantees that timestamps, airport identifiers, and event types are validated before they reach reconciliation. The model below captures a single OOOI event.
from datetime import datetime, timezone
from enum import Enum
from pydantic import BaseModel, field_validator
class OOOIType(str, Enum):
OUT = "OUT" # block-out: moves under own power
OFF = "OFF" # airborne
ON = "ON" # touchdown
IN = "IN" # block-in: at rest after landing
class FlightEvent(BaseModel):
tail_number: str
flight_number: str
origin: str # ICAO, 4 letters
event_type: OOOIType
event_time_utc: datetime
origin_zone: str # e.g. "America/New_York"
source: str # ACARS | FMS | TRACKING
@field_validator("event_time_utc")
@classmethod
def must_be_utc(cls, v: datetime) -> datetime:
if v.tzinfo is None or v.utcoffset() != timezone.utc.utcoffset(None):
raise ValueError("event_time_utc must be timezone-aware UTC")
return v
@field_validator("origin")
@classmethod
def icao_shape(cls, v: str) -> str:
if not (len(v) == 4 and v.isalpha()):
raise ValueError("origin must be a 4-letter ICAO identifier")
return v.upper()
Reconciliation resolves the same logical event arriving from several feeds through a fixed precedence, keeping the choice deterministic and auditable. Block time is then a pure function of the reconciled out and in instants, which is what makes every derived value reproducible from the ledger.
from datetime import timedelta
# Higher wins when two feeds report the same OOOI event.
_SOURCE_PRECEDENCE = {"ACARS": 3, "FMS": 2, "TRACKING": 1}
def reconcile(events: list[FlightEvent]) -> FlightEvent:
"""Pick the authoritative record for one logical OOOI event."""
return max(events, key=lambda e: _SOURCE_PRECEDENCE.get(e.source, 0))
def block_minutes(out_event: FlightEvent, in_event: FlightEvent) -> int:
"""Block-to-block flight time per §117.3, in whole minutes."""
if in_event.event_time_utc <= out_event.event_time_utc:
raise ValueError("block-in must follow block-out")
delta: timedelta = in_event.event_time_utc - out_event.event_time_utc
return int(delta.total_seconds() // 60)
Idempotent synchronisation is what keeps duplicate submissions from double-counting a leg. The pipeline applies a composite key — tail number, origin, and scheduled off-block time — and upserts on conflict, so a retransmitted ACARS burst overwrites rather than appends. Expressed against PostgreSQL, the write is a single conflict-aware statement.
INSERT INTO flight_leg (tail_number, origin, sched_off_block_utc,
block_out_utc, block_in_utc, block_minutes)
VALUES (%(tail)s, %(origin)s, %(sched_off)s,
%(out)s, %(in)s, %(block)s)
ON CONFLICT (tail_number, origin, sched_off_block_utc)
DO UPDATE SET
block_out_utc = EXCLUDED.block_out_utc,
block_in_utc = EXCLUDED.block_in_utc,
block_minutes = EXCLUDED.block_minutes;
Keeping each regulatory quantity — block time, airborne time, duty length — an isolated, testable function is what lets a compliance officer trace any verdict back to a single line of code. The comparison of block time against flight time, and the edge cases that separate them, are developed in depth in Calculating Block Time vs Flight Time in Python.
Rolling Window and Temporal Aggregation
The per-leg block calculation is stateless, but §117.23 is not: it requires rolling totals over 168-hour and 672-hour windows for flight duty period, and 672-hour and 365-day windows for flight time. Timezone drift is the main source of cumulative error, so every event is stamped in ISO 8601 UTC before it enters the aggregation layer. Where the data lives in PostgreSQL, frame-bounded window functions express these caps directly.
-- Rolling 672-hour (28-day) flight-time total per tail, at each leg.
SELECT
crew_id,
block_in_utc,
SUM(block_minutes) OVER (
PARTITION BY crew_id
ORDER BY block_in_utc
RANGE BETWEEN INTERVAL '672 hours' PRECEDING AND CURRENT ROW
) AS rolling_672h_minutes
FROM flight_leg
JOIN crew_assignment USING (leg_id)
ORDER BY crew_id, block_in_utc;
For pre-flight validation inside the scheduler, the same logic runs in memory over Polars or Pandas frames, where a rolling group-by keyed on the block-in timestamp reproduces the SQL frame without a database round trip. Whichever engine is used, the window boundary must be inclusive of the exact regulatory span — an off-by-one on the 672-hour boundary silently under-counts flight time and lets an illegal roster through.
Integration Points
This pipeline is one stage in a longer chain and depends on clean contracts with its neighbours:
- Upstream — data schema validation rules. Structural and semantic gates reject malformed payloads at the edge, so the parser only ever reconstructs legs from records that already conform.
- Sibling — crew roster API integration. Supplies the planned pairings the pipeline reconciles flown legs against; detected drift feeds back as compliance alerts and re-pairing requests.
- Throughput — async batch processing workflows. Decouple parsing from network and database I/O so thousands of concurrent log streams process without blocking the primary dispatch threads.
- Downstream — duty time validation rule engines. Consume the normalised, deduplicated legs this pipeline emits and render the legality verdict against Part 117 and EASA FTL.
- Perimeter — system security and access boundaries. Every parsed record lands in an append-only ledger behind role-based access controls, with a signed audit trail so a disputed block time is non-repudiable.
Testing and Edge Cases
Because parsing logic is stateful and time-sensitive, example-based tests miss the cases that matter. Property-based testing with hypothesis generates thousands of synthetic log streams and asserts invariants — for instance, that reprocessing the same feed twice never changes the ledger (idempotency), and that reconciliation is order-independent. The boundary conditions that most often break flight-log implementations are specific:
- Daylight-saving transitions. A leg whose report time crosses a DST change in
origin_zoneshifts local report hour relative to UTC; the §117.13 FDP band must be selected from the local wall-clock time at report, not from a fixed UTC offset. - Duplicate versus distinct legs. A retransmitted ACARS burst must upsert onto the same composite key, but a genuine same-day turn on the same route must not collapse into it. Tests must assert that a distinct scheduled off-block time always produces a distinct leg.
- Partial OOOI sets. A datalink interruption can deliver
outandoffwithoutonandin. The pipeline must hold the leg open and quarantine it for reconciliation rather than emitting a leg with a fabricated block-in. - 672-hour window edges. A leg landing exactly on the rolling-window boundary must be counted inclusively; property tests should assert the total is invariant to whether the boundary leg is expressed in local or UTC time.
Reconciliation precedence and the derived-quantity functions are validated against recorded operational feeds before every deployment, with the source-precedence constants version-controlled so a change in feed trust is a reviewable diff rather than a silent code edit.
Explore This Topic in Depth
- Parsing ARINC 424 Flight Logs with Python — a step-by-step walkthrough of decoding the fixed-width ARINC 424 navigation format, extracting route and segment data, and mapping it into the canonical event schema this pipeline consumes.
Related
- Flight data ingestion and system sync — the upstream initiative this pipeline belongs to.
- Data schema validation rules — the structural and semantic gates that guard the ingestion edge.
- Crew roster API integration — the planned pairings that flown legs are reconciled against.
- Async batch processing workflows — the concurrency layer that keeps high-volume parsing non-blocking.
- Duty time validation rule engines — the downstream evaluators that consume this pipeline’s output.
Back to Flight Data Ingestion & System Sync.