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.

Flight-log parsing pipeline: source to scheduling ledger Four source feeds converge into a normalise-and-validate stage, which flows into a transform stage that derives block, air and duty metrics, then an idempotent upsert on a composite key, then a stage mapping legs to crew, then a threshold-breach decision. If a threshold is breached the record raises an alert and re-pairs; otherwise the clean leg is written to the scheduling ledger. SOURCES ACARS OOOI ARINC 424 FMS export Tracking API Normalise & validate Transform block · air · duty Idempotent upsert composite key Map legs to crew Threshold breach? breach Alert + recalc pairing clean Scheduling ledger

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.

Parsing pipeline entity-relationship model Five entities and their relationships. raw_log (immutable inbound payload) derives one-to-many flight_event rows. Four flight_event OOOI instants reconstruct one flight_leg. Many flight_legs are grouped into one duty_period. Each flight_leg is mapped to operating crew through one-to-many crew_assignment rows. Every temporal column is stored in UTC. raw_log id · PK source received_utc payload (immutable) flight_event id · PK raw_log_id · FK oooi_type event_time_utc origin_zone flight_leg id · PK tail_number sched_off_block_utc block_minutes duty_period_id · FK duty_period id · PK start_utc end_utc rest_before_h crew_assignment leg_id · FK crew_id · FK role derives reconstructs grouped by mapped by 1 1 1 1

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.

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:

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:

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

Back to Flight Data Ingestion & System Sync.

Explore this section