Lab Technical Report LAB-TR-0020

Extracting Event Categories from Multi-Million-Line Mobile Logs with Predicate Rules: Design Rationale and a Repeatable Rule-Construction Method

Lab Technical Report, preprint, not peer reviewed

Abstract

Mobile examinations produce logs of millions of records, such as the iOS Unified Log and Android bug reports, in which an examiner needs only certain categories of events. Text search is wrong in both directions and fails silently on case variants, NUL bytes and near-miss phrases. We state eight requirements of this category-extraction problem and describe a predicate-rule design that targets them: rules are data, condition trees over a closed set of record fields without regular expressions, compared after NUL removal and Unicode case folding, compiled to parameterized SQL and evaluated in one parallel scan against a frozen, digest-verified pack. We give a ten-step rule-construction procedure with a synthetic worked example. On synthetic corpora of up to five million records the extraction had no false positive or negative and ran at about 300,000 records per second; on seven bug reports and two iOS stores of 24.9 and 72.3 million records it agreed with a separately implemented counter on every label and match, with identical output across runs, worker counts and orders. The measurements also locate its limits: text forms beyond case folding, a second evaluator without the same normalization, and memory that grows with the number of candidate records.

CCS Concepts • Applied computing → System forensics; • Information systems → Query languages.

Keywords log analysis, digital forensics, predicate rules, rule construction, iOS Unified Log, Android bug report, determinism, explainability, Unicode case folding

ACM Reference FormatDaniel Addai, Kingsley Albert Egyir, Sanders Larweh Tetteh, and Selorm Adjabeng. 2026. Extracting Event Categories from Multi-Million-Line Mobile Logs with Predicate Rules: Design Rationale and a Repeatable Rule-Construction Method. Lab Technical Report LAB-TR-0020, version 1.0. SectraTEK Lab, Prudential Associates. Preprint, not peer reviewed. https://lab.sectratek.com/research/reports/lab-tr-0020/

1 Introduction

Mobile examinations produce logs far too long to read. A single iOS sysdiagnose archive can hold tens of millions of Unified Log records (25.6 and 42.6 million in two archives measured in earlier work [2]), and the main text of an Android bug report runs to 0.5 to 2.2 million lines [1]. The examiner rarely needs all of it. The questions are about categories of events: every record in which a given service started a synchronization, every installation record of the package manager, every link change of a network interface. We call the task of returning exactly the records of a written category definition category extraction.

A motivating example. Consider a fictitious background service exampled whose records say sync started or sync finished. A plain text search for the phrase looks adequate and is wrong in both directions (Listing 1). It returns the record sync refused: sync started elsewhere, which is not a synchronization, and the same phrase written by another process. It misses SYNC STARTED, the same phrase spelled with the long s (ſync ſtarted, which Unicode case folding maps to sync started), and a record whose text contains a NUL byte inside the phrase, because the text functions of SQLite stop at the first NUL. A regular expression can repair some of these cases, but on text that a device user or an application can influence it adds a matching cost that the input controls [6]. None of these failures raises an error; each changes a count that may be reported to a court.

Listing 1: Records of a synthetic log and whether each belongs to the category “synchronization of exampled started or finished” (all names and texts invented).

process    message                               in category
exampled   sync started for account 3            yes
exampled   SYNC STARTED for account 4            yes
exampled   ſync ſtarted (long s spelling)        yes
exampled   sync st<NUL>arted after NUL           yes
exampled   batch 7 / sync finished / items: 4    yes (3 lines)
exampled   sync refused: sync started elsewhere  no
otherd     sync started for account 3            no
exampled   sync requested by scheduler           no

The problem. Category extraction from very large forensic logs is a selection problem with requirements that ordinary text search does not meet. The extraction must (i) make one bounded pass over inputs of millions of records, (ii) stay within bounded memory, (iii) treat a record that spans several lines as one unit, (iv) give the same output for the same input and rules on every run, (v) let a reviewer explain every returned record, (vi) not be defeated or stalled by crafted text, (vii) make damaged or incomplete input visible, and (viii) keep the rules readable, reviewable and versioned. Deciding whether an extracted category is suspicious is a separate, later question that this report does not address.

Approach. We describe a predicate-rule design that targets these requirements, a procedure for constructing rules, and an evaluation on the problem itself. A rule is data: a condition tree over a closed set of record fields with a small set of comparisons and no regular expressions, compiled to parameterized statements over a normalized record store and run against a frozen rule pack. We ask six research questions.

  • RQ1 (Design). Which requirements does category extraction from very large forensic logs impose, and which design choices of a predicate language address each?
  • RQ2 (Construction). Can rule construction be a repeatable procedure whose steps produce testable artefacts?
  • RQ3 (Agreement). How closely does the extraction agree with the category definition on synthetic logs with known membership, and with a separately implemented counter on real logs?
  • RQ4 (Determinism). Are results identical across runs, worker counts and rule orders?
  • RQ5 (Cost). What throughput and memory does the design reach on multi-million-record inputs, and how do they change with record count, rule count and the share of non-ASCII text?
  • RQ6 (Robustness). How does the extraction behave on crafted text, on damaged or non-authoritative input, and when a rule is changed without a new version?

Contributions. This report makes four contributions.

  • A statement of the requirements of category extraction from very large forensic logs and a design rationale that ties each requirement to a design choice and to the measurement that tests it (Section 3, Table 2).
  • A ten-step rule-construction procedure in which each step produces a checkable artefact, illustrated with a synthetic worked example and a neutral six-rule pack written for this report (Section 4).
  • An evaluation of a reference implementation on synthetic corpora of up to five million records or lines and on real logs (seven Android bug reports and two iOS Unified Log stores) covering agreement, determinism, throughput, memory, crafted text, damaged input and versioning (Sections 5 and 6).
  • Measured limits of the design and the implementation: text forms that defeat the normalization, a divergence between the two evaluators of the grammar, a blind spot of match-set fingerprints, and three failure paths that are mislabelled or silent (Sections 6.4 to 6.6 and 7).

Scope. The report concerns single-record selection over records that a parser has already produced. Parsing the raw formats is cited from earlier work [1], [2]; sequences across records, joins, aggregation and the judgement of what a category means are outside the design. Other rule and query languages are described from their documentation and were not run or ranked. The real-data evaluation uses one implementation, seven bug reports and two Unified Log stores, and reports only aggregate figures; no production rule is published, and every example rule is new and synthetic.

2 Background and Related Work

2.1 The logs

The iOS Unified Log stores records in binary trace files inside a log archive; a decoder turns each record into fields such as time, process, process identifier, subsystem, category, event type, log level and message text [3], [14]. The decoder used here, Mandiant's unifiedlog_iterator, writes one JSON object per record [14]. A message can span several lines and is one record. An Android bug report is a zip archive whose main text contains, among many service dumps, the logcat buffers: one line per entry with time, user, process and thread identifiers, level, tag and message [9], [1]. In both sources the structural fields (process, subsystem, category, tag, level) are written by the platform; the message is written by the program that logs and can contain any text the program received.

2.2 Rules as data over events

Detection formats express selections over log events as data. A Sigma rule names selections in which a YAML list means OR and a map of field-value pairs means AND, combines them in a condition such as (selection_one or selection_two) and not filter, and refines fields with modifiers such as contains [20]; Hayabusa applies Sigma rules to Windows event logs to build forensic timelines [26]. YARA matches text, hexadecimal and regular-expression strings over content and combines them in a boolean condition [25]. The STIX 2.1 standard defines a patterning language for indicators over observed data [12]. These formats show that shareable, reviewable rules over log records are established practice; the design here keeps that property and narrows the operator set further.

2.3 Query and policy languages

osquery exposes an operating system as a relational database queried with SQL [17], and Velociraptor packages its query language VQL into artifacts that can be shared and run across a deployment [24]. EQL queries event-based time series and supports sequences constrained by a time span [7]; sequences are outside our design. Datalog is a logic-programming query language over relational data [4]; Rego is inspired by it and treats expressions that refer to undefined values as undefined, including comparisons such as != [16]. Cedar argues for a deliberately restricted policy language whose policies can be analysed [5]. The question of what a comparison means when a field is missing, answered differently by these languages and by SQL's three-valued logic [13], is one of the explicit decisions of our design (Section 3.3).

2.4 Log parsing

Log parsing research recovers templates and parameters from free-text messages: Drain parses a stream online with a fixed-depth tree [10], and Zhu et al. benchmark automated parsers [27]. This recovers structure where none is recorded. Category extraction assumes parsed records and selects among them; the two can be combined, but they answer different questions.

2.5 Input-handling hazards and tool testing

Language-theoretic security argues that input-handling code should accept no more expressive a language than its task requires [19]. Regular expressions with super-linear worst-case cost are common in practice and allow denial of service when the input is attacker-influenced [6]. SQLite folds case only for ASCII characters in LIKE by default [21], so a case-insensitive comparison of non-ASCII text needs its own folding; Python's str.casefold implements the default case folding of the Unicode Standard [18], which is distinct from the canonical and compatibility normalization forms [23]. Differential testing compares two implementations of one specification on the same inputs [15]; forensic tool testing literature describes the lack of adequate testing standards for forensic tools [11] and calls for standardized, modular processing [8].

Table 1: Related approaches and the design question each informs. Facts are from the cited documentation; no approach was run or ranked.
ApproachDocumented property used hereDesign question it informsRef.
Sigma, HayabusaSelections as data; list = OR, map = AND; condition with and, or, not; field modifiersRules as reviewable data over events[20], [26]
YARAText, hexadecimal and regular-expression strings; boolean conditionWhich matching power to admit[25]
STIX 2.1 patterningIndicator patterns over observed data (OASIS Standard)Patterns as a standard exchange object[12]
osquery, VelociraptorSQL over the system; queries packaged as shareable artifactsCompiling selections to a relational store[17], [24]
EQLEvent sequences constrained by a time spanWhat is left out (sequences)[7]
Datalog, Rego, CedarDeclarative rules; undefined values propagate; a restricted, analysable languageMissing values; restriction as a design goal[4], [5], [16]
Drain, log-parser benchmarkTemplate recovery from free textA different problem (parsing)[10], [27]

Position of this work. The design does not add a new matching primitive; its components (a condition tree, parameterized SQL, case folding) are known. What we found no description of, in a limited search, is a treatment of category extraction from mobile forensic logs that states the requirements, ties each design choice to a measurement, and gives a repeatable procedure for writing the rules. A systematic literature search is future work.

3 Design

3.1 Requirements and design choices

Table 2 maps each requirement of Section 1 to the design choice that addresses it and to the result that tests it. The table claims that the design is well suited to each need only where a measurement in Section 6 supports it; it makes no claim that other designs cannot meet the same needs.

Table 2: Requirements of category extraction, the design choice that addresses each, and where it is tested.
NeedDesign choiceTested in
N1 One bounded passRules are single-record predicates; all rules are reduced together in one scan of the record store, split into row ranges scanned in parallel, into a candidate table; each rule then reads only the candidatesSec. 6.3
N2 Bounded memoryStatements over the store; scan ranges of at most 25,000 rows; candidates written in chunks of 5,000 and matches in batches of 1,000Sec. 6.3
N3 Multi-line recordsThe parser reassembles a record before any rule sees it; a rule tests one record and never spans recordsSec. 6.1, 6.4
N4 DeterminismRules are pure functions of one record; fixed text normalization; a run freezes the source, the pack digest, the engine and rule versions; matches are immutableSec. 6.2
N5 Explainable matchesEvery match keeps rule key and version, pack digest, engine version, record identifiers and the compiled plan; rules carry description, limitations, sources and validation statusSec. 6.1
N6 Evasion safetyNo regular-expression operator; NUL removed on both sides; full Unicode case folding; wildcard characters escaped; exact path for case-sensitive rules; bounded rule valuesSec. 6.4
N7 Damage visibleA complete, non-partial, approved and digest-verified source and pack are required; a rule failure fails the run; version mismatch warns and never suppressesSec. 6.5
N8 Reviewable, versionedSmall YAML rules with a closed grammar; immutable stored rules; a match-set fingerprint over a frozen corpus forces a version bump when a rule changes behaviourSec. 6.6

3.2 The language

A rule pack is one YAML document with a schema version, pack metadata and a list of rules. A rule has a key and version, a title and description, a validation status (observed, documented only, experimental or deprecated), the operating-system versions it was checked on or is known not to fit, stated limitations, at least one source reference, and a condition. A condition is either a leaf or a group with exactly one of all, any or not (Listing 2). Fields come from a closed list of 24 normalized record fields (19 text, 4 integer, 1 boolean); text fields accept no ordering comparison, integer fields no text matching. Rule content cannot name a table, a column outside the list, a function, a file or a pattern language.

Listing 2: Grammar of a condition (schematic) and the bounds enforced by the loader.

condition := leaf | {all: [condition, ...]}
           | {any: [condition, ...]} | {not: condition}
leaf      := {field: F, operator: OP, value: V,
              case_sensitive: bool (default false)}
OP        := eq | ne | contains | starts_with | ends_with
           | in | exists | gt | gte | lt | lte
bounds: group 1..50 items; in-list 1..1000 values;
  value 4000 chars; pack 10 MiB, 5000 rules;
  YAML: UTF-8, safe loader, no aliases or anchors,
  100,000 nodes, depth 100; unknown keys rejected;
  source URLs http(s) without credentials

3.3 Semantics

Four decisions define what a rule means; each is chosen so that the meaning does not depend on how the text was crafted.

Missing values. A missing field fails every leaf except ne, and not of a failing condition succeeds; exists tests presence. A rule not (message contains "refused") therefore matches a record without a message. SQL would return unknown for such a comparison and drop the record from a WHERE clause [13]; the compiler coalesces the inner result so that the stated rule holds.

Text normalization. Unless a leaf is case-sensitive, both the record value and the rule value are folded with full Unicode default case folding [18], so that STRASSE equals straße and the long s folds to s. NUL bytes are removed from both sides before any comparison, because SQLite text functions stop at the first NUL and a record benign<NUL>marker would otherwise hide marker. No other normalization is applied: canonical or compatibility equivalence [23], zero-width characters and look-alike characters are compared as written (Section 6.4 measures the consequence).

Literal matching. contains, starts_with and ends_with match literally: %, _ and the escape character in a rule value are escaped. A case-sensitive leaf compares bytes exactly (by position and substring functions), not with LIKE, which would fold ASCII case [21]. There is no regular-expression operator, so no rule has super-linear matching cost [6].

Reference semantics. The implementation keeps a reference function that evaluates a condition on one record in Python; its tests check the compiled statements against it. For this report we wrote a further evaluator from the documented rules, without using the compiler or the reference function (Section 5.2).

3.4 Execution

Figure 1 shows the execution of one run. The compiler turns each condition into one parameterized SQL expression and an examiner-visible plan (the condition tree with each leaf's field, operator, value and case mode). The funnel combines all rule conditions with OR and scans the record store once, in row ranges of at most 25,000 rows on parallel read connections (by default up to 32 workers, capped by the number of processors). Rows whose compared text fields are all ASCII are tested directly; the remaining rows are folded once per row and field by a registered deterministic function and then tested. The rows that satisfy any rule are written, with their folded text, into a candidate table keyed by record identifier, so that range order and worker count cannot change its content. Each rule is then evaluated as its own statement over the candidate table on a pool of up to eight threads, and its matches are inserted in batches of 1,000. The full store is therefore read once per run, whatever the number of rules.

Execution of one predicate runA rule pack in YAML passes a strict loader (bounds, closed schema, digests) and a compiler that emits one parameterized SQL expression and a plan per rule. A run is created only from an approved, digest-verified pack and a complete source run, and freezes the source, the pack digest and the engine and rule versions. The normalized record store is read once by the funnel: row ranges of at most 25,000 rows are scanned in parallel; ASCII rows are tested directly and other rows are case-folded once and then tested. Rows that satisfy any rule enter a candidate table keyed by record identifier. Each rule is then one statement over the candidate table, and its matches are written in batches of 1,000 as immutable rows carrying rule key and version, pack digest, engine version, record identifiers and the compiled plan. Rule pack (YAML)data, no code Record storenormalized records Strict loaderbounds, schema, digests Compilerparameterized SQL + plan Funnel: one scanranges in parallel, all rules Candidate tablekeyed by record id Per-rule statementsthread pool, candidates only Immutable matchesbatches of 1,000 Run freezesource, digest, versions Provenance per matchrule, pack, record, plan
Figure 1: Execution of one run. The record store is read once by the funnel, whatever the number of rules; each rule then reads only the candidate table. Dashed arrows carry provenance, not records.

3.5 Provenance and governance

A pack is imported only through the loader and is stored with the SHA-256 digest of its bytes and of its canonical form; each stored rule carries the digest of its canonical definition, and stored rules and matches are protected by database triggers that refuse updates. A run can be created only from an approved, digest-verified pack and a complete, non-partial source run; the stored rules are re-verified against their digests when a run is created and again when it starts. A run records the source run, pack digest, engine version, selected rules and target operating-system version. If any rule fails, the run is marked failed and its matches are not published; if a rule's stated version range does not cover the target version, the run completes with a warning and no match is suppressed. Re-execution creates a new run; review decisions on a match are appended and never change the match.

3.6 Two evaluators of one grammar

The same condition grammar is evaluated a second time, in memory, over the rows that neutral parsers produce from an Android bug report. This evaluator adds a prefix-list operator, states which row types a rule applies to, and writes one label per row and label axis: the first matching rule in file order wins on each axis, and rows that no rule labels on a coverage axis receive an explicit unclassified label, which measures coverage rather than asserting anything about the row. Labels name neutral families of rows; any judgement of what a labelled row means is made in a later stage that this report does not describe. Because rule order is part of this evaluator's semantics, Section 6.2 tests it with shuffled row order, not shuffled rule order.

4 Constructing Rules

A category is only as good as the rule that defines it, and a rule written ad hoc is hard to review. We therefore construct rules by a fixed procedure in which every step leaves an artefact that a reviewer can check (Table 3).

Table 3: The ten-step rule-construction procedure and the artefact each step produces.
StepWhat is doneArtefact
1 DefineOne sentence of what is in and what is out; the unit (one record); the question the category answers; why a match is context, not a conclusionCategory card
2 Collect evidenceFind records of the category on at least two operating-system versions, or in public documentation or public tool output; note where each was seenEvidence table with counts per version
3 Choose anchorsPrefer structural fields the log itself records (process, subsystem, category, tag, event type) over message phrases; use message text only to narrow; check each anchor is stable across versionsAnchor list with stability note
4 Write the conditionOnly all, any, not and leaves; short, specific values; one leaf per alternative so that each is visible in the planYAML rule
5 Add negativesCollect near-misses that share the anchor but are not the category (refusals, requests, retries, status polls) and records of adjacent vocabulary; turn them into not branches or tighter anchorsNear-miss fixtures
6 State limits and statusWhat a match does not prove and what absence does not prove; validation status (observed only if seen on real records); version range checkedLimitations, status, version range
7 TestPositive and near-miss fixtures, and one-field-changed variants of each, give the intended match set; compiled statement agrees with the reference functionPassing fixture tests
8 MeasureCount matches on a real corpus; compare with an independent count; review a stratified sample for precision with a second reviewerAgreement and precision record
9 Version and freezeRecord the match-set fingerprint over the frozen corpus; any change of match set needs a new version; build the pack, record its digest, approve itFingerprint, pack digest, approval
10 Re-validateOn a new operating-system version, re-run steps 7 and 8; a failure lowers the status or narrows the version range and never silently widens the ruleUpdated status and range

4.1 Worked example

We illustrate the procedure with category A of a neutral pack written for this report: background synchronization of the fictitious service exampled. Every name, text and count in this subsection is invented; none comes from a production rule.

Step 1. In: records in which exampled says a synchronization started or finished. Out: records saying a synchronization was requested or refused. A match shows that the service wrote the record, not that data moved. Step 2. A synthetic log of 300,000 records contains 140 category records written in seven forms (Table 4). Step 3. The anchor is the structural field process_name; the message only narrows. Step 4. Listing 3 shows the rule. Step 5. The near-miss sync refused: sync started elsewhere becomes the not branch; a record of another process with the same text is excluded by the anchor. Step 6. Limitation: a synchronization that fails before it logs is not seen; status experimental, because the records are synthetic. Step 7. The fixtures of Table 4 include case variants, the long s, a NUL byte inside the phrase, a three-line message and a record without a message. The last is a useful test of the missing-value rule: its not branch succeeds, but the record is still excluded because the any branch fails. Step 8. The independent count is the generator's own record of which records it wrote as category members. Steps 9 and 10 are exercised by the mutation test of Section 6.6.

Listing 3: The worked-example rule (category A), as written in the neutral pack.

key: neutral.a.exampled-sync
version: 1.0.0
validation_status: experimental
limitations:
- A synchronization that fails before it logs
  is not seen.
condition:
  all:
  - {field: process_name, operator: eq,
     value: exampled}
  - any:
    - {field: message, operator: contains,
       value: sync started}
    - {field: message, operator: contains,
       value: sync finished}
  - not: {field: message, operator: contains,
          value: sync refused}

Listing 4: The statement compiled from Listing 3 (abridged). fold() is a registered deterministic SQL function that removes NUL and applies Unicode case folding; G leaves ASCII text to LIKE, which folds ASCII case itself.

F(x) := CASE WHEN length(x) <> length(CAST(x AS BLOB))
          THEN fold(x) ELSE lower(x) END
G(x) := CASE WHEN length(x) <> length(CAST(x AS BLOB))
          THEN fold(x) ELSE x END

    (F(process_name) = ?1)
AND ((G(message) LIKE ?2 ESCAPE '\')
  OR (G(message) LIKE ?3 ESCAPE '\'))
AND (NOT COALESCE((G(message) LIKE ?4 ESCAPE '\'), 0))

?1 = 'exampled'          ?2 = '%sync started%'
?3 = '%sync finished%'   ?4 = '%sync refused%'
Table 4: Worked-example forms of category A and how each evaluator treated them. Record store: 20 records per form in the 300,000-record corpus (engine and counter identical). Logcat: 100 lines per form in a 200,000-line synthetic bug report.
FormIn categoryRecord storeLogcat
sync started for account 3yes20 of 20100 of 100
sync finished in 412 msyes20 of 20100 of 100
SYNC STARTED (upper case)yes20 of 20100 of 100
Process name EXAMPLEDyes20 of 20100 of 100
Three-line messageayes20 of 20100 of 100
Long s: ſync ſtartedyes20 of 200 of 100
NUL inside the phraseyes20 of 200 of 100
sync refused: sync started elsewhereno0 of 200 of 100
Same text, process otherdno0 of 200 of 100
sync requested by schedulerno0 of 200 of 100
No messageno0 of 200 of 100

a A logcat line carries one line; the three lines were joined with spaces in the bug-report form.

The neutral pack has six rules, categories A to F, each over fictitious processes: A synchronization (above), B power-state changes of a fictitious controller, C interface link changes, D package installations, E process lifecycle records with a case-sensitive category anchor, and F fault-level records of example subsystems from user processes. Together they use 21 leaves: eq 7 (one case-sensitive), contains 5, starts_with 4, in 2, and ends_with, gte and exists one each, in 6 all, 3 any and 3 not groups. The pack is the only operator distribution reported here.

5 Evaluation Method

Every figure in Section 6 comes from one evaluation runner that executed each measurement in a fresh process with its own time limit.

5.1 Evidence

Synthetic corpora. A generator written for this report produces records of fictitious processes with random filler text and plants, at fixed intervals, one round of 34 records: the positive forms and near-misses of the six categories (Section 4.1). The generator records which planted records belong to which category from the category cards, never by evaluating a rule; this record is the generator truth. Corpora: the worked example (300,000 records, 140 category-A records); 1, 2 and 5 million records (one round per 2,000 records); 1 million records with 10 and 50 percent of the filler records carrying non-ASCII words and process names; and 1 million records with packs of 10, 100 and 1,000 rules (the six category rules plus generated rules over further fictitious processes, each with one planted record). For the in-memory evaluator, the same generator writes the records as logcat lines into synthetic bug reports of 0.2, 1, 2 and 5 million lines, built with the header builder of the implementation's own synthetic test suite; categories A, C and D are re-expressed over the logcat tag, because a logcat line has no subsystem, category or log-type field.

Constructed cases. A suite of 31 text cases (42 records) for crafted text (Section 6.4); 16 malformed packs for the loader; 14 cases of damaged or non-authoritative sources and of faults injected during execution; 5 damaged variants of a synthetic bug report (Section 6.5); and 157 mutants of the six neutral rules (Section 6.6).

Real logs. Seven Android bug reports from four vendor families (Transsion-brand, Huawei, Motorola and four Samsung reports; Android 10, 15 and 16), the same set studied in earlier work [1], and two iOS Unified Log stores decoded from the sysdiagnose archives of two iPhones (iOS 26.6 and 26.6.2). The archives were decoded with unifiedlog_iterator v0.6.0 [14] and each decoded record was normalized by the reference implementation's own normalization function into its record store. A third archive was not used because its compressed stream ends early (an incomplete capture), and an iOS 27.0 archive was not used because this decoder version recovers only a small share of its records [2]. On the real logs the rule pack is a production pack of the reference implementation; we publish no rule from it, no count of its rules and no per-rule result. The two stores held 24,925,400 and 72,336,796 records; 10.0 and 39.7 percent of them had multi-line messages, and every decoded line was a valid JSON record.

5.2 Measures and references

Agreement (RQ3). On synthetic corpora the reference is the generator truth; we report true and false positives and false negatives per category. On real logs no record-level truth exists, so the reference is a separately implemented counter: an evaluator of the documented semantics (Section 3.3 for the record store, Section 3.6 for parsed bug-report rows) written for this study without the compiler or the reference function, applied to every record of the store read with plain SQL (iOS) or to every parsed row (Android). We compare the sets of (rule key, rule version, record identifier) or (row, axis, label, rule key). This measures agreement with the specification, not truth, and both sides read the same parsed records.

Determinism (RQ4). Each run's matches are written as a sorted listing of (rule key, rule version, stable record identifier) and hashed with SHA-256. We compare listings across three default runs in fresh processes, a run with one funnel worker and one rule worker, a run with eight of each, and a run of the same rules in shuffled order. For the in-memory evaluator, the listing of (row, axis, label, rule key) is compared across two fresh-process runs and a run with shuffled row order.

Cost (RQ5). Execution time is the wall time of one run from start to completion, including writing and publishing the matches; the funnel time is taken from the run's own event log. Memory is the peak working set of the measuring process. Store building is a separate process and is not included. The ratio for non-ASCII text is the best of two runs at each share against the best of two runs at 0 percent.

Robustness (RQ6). For each crafted case we state the intended category membership in advance and compare it with the engine and the counter. For damaged sources the pass criterion is “no silent completion”: a run must complete correctly, be refused, fail, or complete with a warning. For versioning, each mutant's match set is computed over the frozen corpus (fixtures, one-field-changed variants and value-derived near-misses, as in step 7) and over a reference corpus of 250,605 records; a mutant changes behaviour if its match set on the reference corpus differs, and is caught if its fingerprint on the frozen corpus differs.

Not measured. Precision on a stratified sample labelled by two reviewers (step 8) was not carried out; agreement with the counter is reported instead.

5.3 Environment

One workstation: AMD Ryzen Threadripper 3990X (64 cores, 128 logical processors), 256 GB of memory, Windows 10 Pro for Workstations. Python 3.13.15, SQLite 3.50.4, pydantic 2.13.4, PyYAML 6.0.3; reference implementation engine 1.0.0 from one pinned, unmodified source revision; unifiedlog_iterator v0.6.0. Default worker settings unless stated. Real evidence was read in place and copied only into a scratch directory that was deleted after the runs.

6 Results

Table 5 answers each research question.

Table 5: Answers to the research questions.
QuestionAnswerEvidence
RQ1 DesignEight requirements, each addressed by a stated design choice and tested by a measurement below.Sec. 3, Table 2
RQ2 ConstructionYes: ten steps, each with a checkable artefact; the worked example produced them on synthetic data, steps 9 and 10 were exercised by the mutation test, and the two-reviewer precision review of step 8 was not done.Sec. 4, Tables 3 and 4
RQ3 AgreementComplete agreement: no false positive or negative on any synthetic corpus for the record-store engine; identical labels and matches to the separately implemented counter on all seven bug reports and both iOS stores (8,927,057 matches on the larger). The in-memory evaluator missed 2 of 7 synthetic positive forms.Sec. 6.1, Table 6
RQ4 DeterminismYes, in every comparison: six runs of 5 million records (repeats, 1 and 8 workers, shuffled rules), default and single-worker runs of an iOS store, and four runs of each bug report (including shuffled rows).Sec. 6.2
RQ5 CostAbout 300,000 records per second on synthetic corpora with peaks below 450 MB, and 271,000 on a 24.9-million-record store; cost and memory follow the number of candidate records: 8.9 million candidates on a 72.3-million-record store took 55 min and 29.9 GB. Packs of 1,000 rules exceeded SQLite's expression-depth limit; 50 percent non-ASCII text cost 5.0 times as much.Sec. 6.3, Figures 2 and 3
RQ6 Robustness27 of 31 crafted cases as intended; the 4 failures need normalization beyond case folding. 16 of 16 malformed packs rejected. No silent completion in the record-store engine, one failure mislabelled; two silent losses in the bug-report path. Fingerprints caught 93 of 97 behavioural changes, with no false alarm.Sec. 6.4 to 6.6, Tables 7 and 8

6.1 Agreement (RQ3)

Synthetic corpora. On the worked example the engine returned exactly the 340 planted category records (140 of category A, 40 of each of B to F) and nothing else; the separately implemented counter returned the same 340 records. Every positive form was found and every near-miss excluded (Table 4). On the corpora of 1, 2 and 5 million records the engine returned 8,504, 17,004 and 42,504 matches with no false positive and no false negative against the generator truth (Table 6). Every match carried rule key and version, pack digest, engine version, record identifiers, the matched condition and a non-empty compiled plan, and on the worked example each of the 340 matched records resolved to exactly one raw record.

In-memory evaluator. On synthetic logcat text the in-memory evaluator agreed with the separately implemented counter on every label, and with the generator truth for categories C and D. For category A it found 5 of the 7 positive forms: the long-s spelling and the phrase split by a NUL byte were not labelled (Table 4), because this evaluator lowers case instead of folding it and keeps NUL bytes. On 200,000 lines this is 200 of 700 category-A lines missed; the record-store engine finds both forms (Section 6.4).

Real logs. On all seven bug reports the production label pack and the counter produced identical label sets, from 75,219 to 1,161,445 labels per report, with every rule-level count equal. On the first iOS store (24.9 million records) every one of the 153,875 matches of the production pack was also found by the counter and the counter found no other, again with every rule-level count equal. The matched records were 0.6 percent of that store. On the second store (72.3 million records) the two agreed on all 8,927,057 matches. On both stores every distinct matched record resolved to exactly one raw record.

Table 6: Record-store engine on synthetic corpora (default workers, first run).
CorpusMatchesFPFNRun (s)Funnel (s)Peak (MB)
0.3 M (worked example)340000.470.20106
1 M8,504003.231.13198
2 M17,004007.823.00286
5 M42,5040015.207.20328

FP, FN: false positives and false negatives against the generator truth. Run: wall time from start to completion, including writing and publishing the matches.

6.2 Determinism (RQ4)

On the 5-million-record corpus, six runs gave the same listing hash (Figure 3c). On the first iOS store, the default run and the single-worker run gave the same hash over 153,875 matches. On each of the seven bug reports, two fresh-process runs, a run with shuffled row order and the counter run gave the same label listing, and two runs of each synthetic report of 1 and 5 million lines gave the same listing. We observed no difference in any comparison.

6.3 Cost (RQ5)

On synthetic corpora run time grew roughly in proportion to the input, from 3.2 s for one million records to 15.2 s for five million (about 300,000 records per second), and peak memory stayed below 450 MB (Table 6, Figure 2). The funnel scan took 35 to 47 percent of these runs; the rest is the per-rule phase and the writing and publication of matches. On real stores with the production pack, cost followed the number of candidates, the records that satisfy at least one rule, more than the number of records. The 24.9-million-record store (153,782 candidates) ran in 91.9 s (271,000 records per second; funnel 62.7 s) with a peak of 1.13 GB. The 72.3-million-record store, on which 12.3 percent of the records were candidates (8,925,432), needed 1,190.7 s for the funnel, 1,124.6 s for the per-rule phase and 3,303.7 s including publication, with a peak of 29.9 GB. The funnel holds its candidate rows in memory before writing them, so requirement N2 holds only while candidates are few: memory is bounded by the candidate count, not by a constant. We did not establish why the funnel time grew 19-fold for 2.9 times as many records; the share of multi-line messages (39.7 against 10.0 percent) and the candidate volume are the two differences we observed.

Parallel scanning carried most of the speed. On the iOS store a single worker took 1,245.5 s against 91.9 s with default settings, 13.6 times longer. On the 5-million-record corpus one worker took 28.0 s, eight workers 14.0 s and the default (up to 32) 15.2 s; the funnel itself was fastest with eight workers (3.9 s against 7.2 s), so more workers than eight did not help on this input. With 100 rules instead of 10, one million records took 4.8 against 2.6 s; the funnel time hardly changed (1.21 against 1.05 s) and the increase lies in the per-rule phase. With 1,000 rules the run failed at once: the funnel combines all conditions into one statement, and that statement exceeded SQLite's default expression-depth limit of 1,000 [22]. The run was marked failed with an engine-failure code, so the failure is visible, but the loader accepts packs of up to 5,000 rules, so the bound that matters for the funnel is not the one the loader enforces. Non-ASCII text costs more because each such value is folded by a function call. With 10 and 50 percent of the filler records carrying non-ASCII words and process names, one million records took 1.74 and 5.02 times as long as with none (best of two runs, Figure 3b); the match listings were the same at every share. The implementation's own performance budget (at most 1.5 times) compares its folding scan with an ASCII-only scan of the same rows; our runs compare one engine across shares, so they measure the cost of non-ASCII text itself rather than that budget.

For the in-memory evaluator, labelling the parsed rows of the real bug reports took 1.0 to 24.0 s per report (28,800 to 112,900 rows per second), after parsing that took 2.9 to 21.7 s; the measuring harness held all parsed rows, and peak memory was 201 to 1,392 MB. With streaming labelling, in which no row is retained, the synthetic reports of 1 and 5 million lines took 25.9 and 141.7 s including parsing (about 35,000 to 39,000 lines per second) with peaks of 310 and 1,286 MB; retaining all rows raised the 5-million-line peak to 5,582 MB.

Throughput and peak memory against input sizeTwo bar charts for the record-store engine with default workers. (a) Throughput in thousand records per second, run time including writing and publishing matches: 0.3 M synthetic: 643; 1 M synthetic: 310; 2 M synthetic: 256; 5 M synthetic: 329; 24.9 M iOS 1: 271; 72.3 M iOS 2: 22. (b) Peak working set in GB with the number of funnel candidates: 0.3 M synthetic: 0.10 GB, 340 candidates; 1 M synthetic: 0.19 GB, 8504 candidates; 2 M synthetic: 0.28 GB, 17004 candidates; 5 M synthetic: 0.32 GB, 42504 candidates; 24.9 M iOS 1: 1.13 GB, 153782 candidates; 72.3 M iOS 2: 29.93 GB, 8925432 candidates. Synthetic corpora use the six-rule neutral pack; iOS stores use a production pack.(a) Throughput, thousand records/s02004006008000.3 M synthetic6431 M synthetic3102 M synthetic2565 M synthetic32924.9 M iOS 127172.3 M iOS 222(b) Peak memory, GB (candidates)081624320.3 M, 340 cand.0.101 M, 8,504 cand.0.192 M, 17.0 k cand.0.285 M, 42.5 k cand.0.32iOS 1, 153.8 k cand.1.13iOS 2, 8.9 M cand.29.9
Figure 2: Record-store engine, default workers: (a) throughput, including writing and publishing matches; (b) peak memory, which follows the number of funnel candidates rather than the number of records. Synthetic corpora (dark) ran the six-rule neutral pack; iOS stores (accent) ran a production pack.
What changes the costThree bar charts. (a) Run time on one million synthetic records by pack size: 10 rules: 2.55 s; 100 rules: 4.77 s; 1,000 rules: the run failed because the combined statement exceeded SQLite's expression depth limit. (b) Run time at 10 and 50 percent non-ASCII filler records relative to 0 percent (best of two runs): 0 % non-ASCII: 1.00 times; 10 %: 1.74 times; 50 %: 5.02 times. (c) Funnel time on five million records by worker setting: 1 worker: funnel 15.3 s, run 28.0 s; 8 workers: funnel 3.9 s, run 14.0 s; default: funnel 7.2 s, run 15.2 s. All runs gave identical listings.(a) Rules in the pack (1 M records), run s0246810 rules2.55100 rules4.771,000 rulesrun failed: SQL depth limit(b) Non-ASCII share (1 M records), run time ratio0x1x2x3x4x5x6x0 % non-ASCII1.00x (2.5 s)10 %1.74x (4.4 s)50 %5.02x (12.8 s)(c) Funnel workers (5 M records), funnel s051015201 worker15.3 (run 28.0)8 workers3.9 (run 14.0)default7.2 (run 15.2)
Figure 3: Cost factors on synthetic corpora: (a) number of rules, (b) share of non-ASCII records, relative to 0 percent, (c) funnel workers. Best of two runs in (a) and (b).

6.4 Crafted text (RQ6)

Table 7 groups the 31 crafted-text cases. The engine and the counter agreed on all 42 records, and 27 of the 31 cases met their intended outcome. NUL bytes, case variants handled by full case folding, literal wildcard characters, line structure, missing values, lists and very long values behaved as specified. The four failures are text forms that case folding does not equate: a dotted capital I, fullwidth letters, a decomposed accented letter and a zero-width space inside the phrase; each of them hides a record from a rule written in plain lower case. All 16 malformed packs were rejected by the loader before any rule was stored: YAML aliases and a Python object tag, unknown keys at three levels, an unknown field, a regular-expression operator, a string for an integer field, an ordering comparison on text, a group of 51 items, nesting beyond the depth limit, a value over 4,000 characters, an in list over 1,000 values, two group kinds in one node, a non-HTTP source URL, a duplicate rule version and invalid UTF-8.

No rule can be made to backtrack, but literal matching still has a worst case. A contains test of a 10-character value against 200 records of 1 MB of repeated a took 3.71 s, 17 times the 0.22 s on the same volume of ordinary text; with a 20-character value the factor was 29. The cost grows with the length of the rule value and of the text, which are both bounded (rule values at 4,000 characters), not with a structure the input controls.

Real logs contain the forms the normalization targets: of the 24.9 million records of the first iOS store, 2.49 million had multi-line messages, 200,864 had non-ASCII text in the message, and 5 had a NUL byte in the message.

Table 7: Crafted-text cases, grouped. Each case anchors one leaf to a set of synthetic records whose intended membership was fixed in advance. Engine and counter agreed on every record.
GroupCasesEngine = intended
NUL bytesInside a phrase; leading; in the rule value; in an eq field; with a case-sensitive leaf5 of 5
Case foldingUpper and mixed case; sharp s against SS; long s; Greek final sigma; Kelvin sign6 of 6
Beyond case foldingDotted capital I (İSTANBUL against istanbul); fullwidth letters; decomposed against composed é; zero-width space inside a phrase0 of 4
Look-alike letterCyrillic а in place of Latin a (intended: not in category)1 of 1
Literal matching%, _, backslash, a regular-expression-like value; case-sensitive exact match5 of 5
Line structureCR LF inside and at the end of a phrase; a three-line record2 of 2
Missing valuesnot, ne and contains on a missing field; exists on an empty string4 of 4
Lists and numbersin with case variants; gte at the bound2 of 2
SizeValue at the end of a 1 MB message; 1 MB of repeated text2 of 2

6.5 Damaged and non-authoritative input (RQ6)

Table 8 lists the outcomes. In the record-store engine no case completed silently: incomplete, failed or running sources and unapproved packs were refused; rule rows edited in the database file after import were refused by the digest check both before and after run creation; ordinary updates of stored rules were refused by the immutability trigger; a missing store and a rule failure failed the run; a version range that did not cover the target completed with a warning. One path is weak: when a rule fails after writing matches, the clean-up of those matches is itself refused by the immutability trigger, so the run fails with a generic engine-failure code instead of the rule-failure code, and the failed rule's 70 matches remain in the store, unpublished. In the bug-report path, CRLF line ends, NUL bytes and a missing end-of-report marker left the labels unchanged; a text cut at 99.5 percent lost the 1,001 lines after the cut (9 labels) without a warning; and a zip cut at 60 percent was not recognised as a zip, was read as text, and completed with no rows and no labels.

Table 8: Damaged, non-authoritative and faulty input: outcome and whether the result says so.
CaseOutcomeVisible
Record-store engine (20,000-record synthetic store)
Clean source (control)Completed—
Source run marked partialRefused (source not authoritative)Yes
Source run still runningRefusedYes
Source run failedRefusedYes
Source completed with warningsCompletedSource status kept
Pack imported but not approvedRefusedYes
Stored rule updated through the databaseUpdate refused by immutability triggerYes
Stored rule edited in the file, before run creationRefused (digest mismatch)Yes
Same edit after creation, before executionRefused (digest mismatch)Yes
Store file missing at executionRun failedYes
Rule fails before writing matchesRun failed, rule-failure code; nothing publishedYes
Rule fails after writing matchesRun failed, generic engine-failure code; 70 matches of the failed rule kept, unpublishedYes, mislabelled
Target version outside the rule's rangeCompleted with warning; no match suppressedYes
Re-execution of a finished runRefusedYes
Bug-report path (200,000-line synthetic report, neutral labels)
CR LF line endsCompleted; listing identical to the clean report—
NUL bytes in 201 further linesCompleted; labels unchanged—
End-of-report and duration markers removedCompleted; labels unchangedNo warning
Text cut at 99.5 percentCompleted; 1,001 lines and 9 labels lostNo
Zip cut at 60 percentRead as text; completed with 0 rows and 0 labelsNo

6.6 Versioning (RQ6)

We generated 157 mutants of the six neutral rules: 48 intended broadenings, 46 narrowings, 18 other changes and 45 neutral edits (reordered groups, upper-cased values of case-insensitive leaves, duplicated list values, edited titles and limitations, a single-item wrapper). On the 250,605-record reference corpus, 97 mutants changed behaviour and 15 non-neutral mutants did not. The fingerprint over the frozen corpus of 605 records caught 93 of the 97 behavioural mutants (95.9 percent), changed for none of the 15 behaviour-preserving ones, and raised no false alarm on the 45 neutral edits. The four misses were two added alternatives with new vocabulary (categories A and E), an ends_with widened to contains (B) and an eq widened to contains (C): each widens the rule only toward text that the value-derived near-misses do not contain.

7 Discussion

7.1 What the design buys

The measurements support the claim that a restricted predicate language is well suited to category extraction from very large logs on the needs it targets. Because rules are data with a closed grammar, the engine and a counter written from the documentation could be compared record by record, and they agreed everywhere they were compared. Because each rule tests one record, all rules could be reduced in one scan of the store. Because no rule can express a regular expression, the worst crafted text we tried cost 17 to 29 times ordinary text, a factor that grows with the length of the rule value rather than with a structure the input controls. Because a run is frozen and matches are immutable, identity of output across runs, worker counts and rule orders is checkable by a hash.

7.2 Normalization is part of the category definition

The crafted-text cases show that “case-insensitive” is not one thing. Full case folding catches forms that ASCII lowering misses (the long s, the sharp s, the Kelvin sign), but it does not equate canonically equivalent sequences, compatibility forms such as fullwidth letters, text with a zero-width character inserted, or look-alike letters from another script. Whether a fullwidth or decomposed spelling should be in a category is a decision about the category, not a parser detail; a rule author who needs it must say so, and the engine would need a declared normalization form to honour it. The two evaluators of one grammar also differ: the record-store engine removes NUL and folds case fully, the in-memory evaluator lowers case and keeps NUL. A grammar shared by two evaluators therefore needs one normalization function shared by both, and a conformance test that runs the same cases through each.

7.3 What fingerprints can and cannot see

Match-set fingerprints caught every narrowing and most broadenings, with no false alarm on edits that kept the behaviour. The four misses have one cause: the frozen corpus is derived from the rule's own values, so it contains no record of vocabulary the rule has never mentioned. A rule broadened to also match sync resumed keeps its fingerprint unless the corpus already holds such a record. Step 5 of the procedure therefore asks for near-misses of adjacent vocabulary, not only for variants of the rule's own text; with that addition the missed cases become detectable, but no finite corpus makes the check complete.

7.4 Failures that are mislabelled or silent

No damaged or non-authoritative source led to a silent completion in the record-store engine, but one path is visible and mislabelled. The bug-report path is weaker: a text cut short loses its last lines without a warning, and a zip cut inside its member is not recognised as a zip, is read as text, and completes with no rows and no warning at the stage we measured. All three are implementation defects, not properties of the design, and they are of the kind that earlier work found in bug report parsing [1].

7.5 Memory and statement size follow the pack

The single funnel scan meets requirement N1, but the reference implementation keeps the funnel's candidate rows in memory before it writes them to the candidate table. With a selective pack that is cheap (1.13 GB for 154 thousand candidates); with a pack in which one broad rule selects one record in eight it is not (29.9 GB for 8.9 million candidates). Writing each scan range's candidates as soon as the range finishes would bound memory by the range size instead; we have not built or measured that change. The funnel also combines every condition of the pack into one statement, which SQLite refused at 1,000 rules; evaluating the funnel in groups of rules would keep one scan per group and bound the statement, at the cost of more scans. Until then, the practical pack size of the funnel is set by the database's expression-depth limit, not by the loader's limit of 5,000 rules.

7.6 What predicates cannot express

The language cannot express an ordered sequence of records, a join between sources, a count or rate, or a pattern over text structure. Categories that need these (a lock followed by an unlock within a minute, a burst of failures) are outside the design and belong to a later stage that reads the extracted categories. The restriction is what makes the per-record properties above checkable, and the price is that some examiner questions need two stages.

8 Threats to Validity

Internal validity. The same authors wrote the neutral rules, the generator, the fixtures and the separately implemented counter, with AI assistance; independence is by method, not by person, and a shared misreading of the documented semantics would appear on both sides. On real logs, engine and counter read the same parsed records, so agreement says nothing about parsing or decoding.

Construct validity. Agreement with the counter measures conformance to the specification, not that a category was extracted correctly from the device's activity. Generator truth reflects our assumptions about how log text varies. The intended outcomes of the crafted cases are our judgement of what the category should contain.

External validity. One implementation and one record schema were evaluated. The real evidence is seven bug reports from four vendor families and two Unified Log stores from two iPhones on iOS 26.6 and 26.6.2; other releases, vendors and log sources were not examined, and an iOS 27.0 archive could not be decoded completely with the decoder version used. The neutral categories may be easier than real ones.

Conclusion validity. Timings are single or paired runs on one workstation and depend on hardware and on SQLite behaviour; we make no statistical inference. The 19-fold growth of funnel time on the larger iOS store was not explained. The second iOS store was run once, with default settings only, for reasons of time. Determinism is largely by construction for pure functions of one record, and the identity of hashes should not be read as more than that.

Limitations. Precision on a stratified sample labelled by two reviewers (procedure step 8) was not carried out, and recall on real logs has no oracle. No other rule or query language was run. The memory figures of the in-memory evaluator on real reports include all parsed rows, which the measuring harness held. The production rules that ran on the real logs are not published, so their agreement figures can be checked only with comparable rules and logs.

9 Conclusion and Future Work

Extracting categories of events from multi-million-record mobile logs needs more than text search: one bounded pass, bounded memory, whole records, identical output, explainable matches, resistance to crafted text, visible damage and reviewable rules. A restricted predicate language compiled to parameterized SQL is well suited to these needs as measured here: it agreed with the category definition on synthetic corpora and with a separately implemented counter on seven bug reports and two iOS stores of up to 72.3 million records, gave identical output under every variation we tried, and needed one parallel scan of the store. A ten-step construction procedure makes each rule a set of checkable artefacts. The same measurements show where the design stops: normalization beyond case folding, a second evaluator that must share the normalization, memory that follows the number of candidates, and fingerprints that cannot see vocabulary they were never given. The requirements and the procedure apply to any rule engine over parsed logs, open-source tools included.

Future work, in order of value: (1) a labelled precision study on real logs with two reviewers; (2) one normalization function and a shared conformance suite for both evaluators, with an optional declared normalization form per rule; (3) adjacent-vocabulary near-misses in the frozen corpus, and a repeat of the mutation test; (4) repair and re-test of the three weak failure paths; (5) more releases and vendors, including iOS 27 with a decoder that reads its compression; (6) a systematic literature search before any submission.

Artifact Availability

The reference implementation and its production rule packs are not released; no production rule, rule count or per-rule result appears in this report. The neutral six-rule pack, the worked example, a seeded generator of the synthetic corpora with generator truth, two evaluators of the condition grammar written from the semantics stated in Sections 3.3 and 3.6, the 31 crafted-text cases and the mutation test of Section 6.6 will be released in the Lab's public artifacts repository, with no release date set (code under the MIT licence, data and text under CC BY 4.0, Python standard library only). The package is not public yet, and the sentences that follow describe it as planned. With them the synthetic results can be reproduced, with the evaluators in place of the engine: Table 4, the match counts of Table 6, Table 7 and Section 6.6. The timings and memory of the engine, the loader rejections, the damaged-input outcomes of Table 8 and everything measured on the real logs are not reproducible from the release; the real logs cannot be released.

Data Handling Statement

The evidence for this study is drawn from the Lab's research corpus of Android bug reports and iOS sysdiagnose archives and was read only. It also uses fully synthetic records, logs and rule packs written by the authors with invented names and texts under example domains. The report gives aggregate counts, device classes, operating-system versions, timings and agreement figures. It contains no identifiers, personal data, message or file content, or case information; no record text, process list, identifier or rule content from the real data or from a production rule pack appears in it. The crafted-text cases are generic and defensive and name no tool.

Declaration of AI Assistance

The authors conceived the research questions and directed the work. The evaluation runner, the synthetic generator, the separately implemented counter, the data checks and the first draft of this report were produced with AI assistance (Anthropic Claude). The authors reviewed the results and the text and are responsible for the content.

References

  1. Daniel Addai. 2026. Making Silent Failures Visible: A Single-Pass Section Reader for Android Bug Reports. Lab Technical Report LAB-TR-0001, version 1.0. SectraTEK Lab, Prudential Associates. https://doi.org/10.5281/zenodo.23717910
  2. Daniel Addai. 2026. Log Compression and Archive Structure in the iOS 27.0 Sysdiagnose: An Early Observation Tested Against Ten Archives. Lab Technical Report LAB-TR-0011, version 1.0. SectraTEK Lab, Prudential Associates. https://doi.org/10.5281/zenodo.23717935
  3. Apple. [n. d.]. Logging. Apple Developer Documentation. Retrieved October 27, 2026 from https://developer.apple.com/documentation/os/logging (Title confirmed; page body not machine-readable, not re-verified.)
  4. Stefano Ceri, Georg Gottlob, and Letizia Tanca. 1989. What you always wanted to know about Datalog (and never dared to ask). IEEE Transactions on Knowledge and Data Engineering 1, 1 (1989), 146–166. (Bibliographic data confirmed from index records.)
  5. Joseph W. Cutler, Craig Disselkoen, Aaron Eline, Shaobo He, Kyle Headley, Michael Hicks, Kesha Hietala, Eleftherios Ioannidis, John Kastner, Anwar Mamat, Darin McAdams, Matt McCutchen, Neha Rungta, Emina Torlak, and Andrew Wells. 2024. Cedar: A New Language for Expressive, Fast, Safe, and Analyzable Authorization. Proceedings of the ACM on Programming Languages (OOPSLA 2024). Extended version: arXiv:2403.04651
  6. James C. Davis, Christy A. Coghlan, Francisco Servant, and Dongyoon Lee. 2018. The impact of regular expression denial of service (ReDoS) in practice: An empirical study at the ecosystem scale. In Proceedings of the 26th ACM Joint European Software Engineering Conference and Symposium on the Foundations of Software Engineering (ESEC/FSE 2018). ACM, 246–256. https://doi.org/10.1145/3236024.3236027 (Bibliographic data confirmed from index records.)
  7. Elastic. [n. d.]. EQL. Elasticsearch Guide. Retrieved October 27, 2026 from https://www.elastic.co/guide/en/elasticsearch/reference/current/eql.html
  8. Simson L. Garfinkel. 2010. Digital forensics research: The next 10 years. Digital Investigation 7 (2010), S64–S73. https://doi.org/10.1016/j.diin.2010.05.009 (Bibliographic data confirmed from index records; full text not read.)
  9. Google. [n. d.]. Capture and read bug reports. Android Developers. Retrieved October 27, 2026 from https://developer.android.com/studio/debug/bug-report
  10. Pinjia He, Jieming Zhu, Zibin Zheng, and Michael R. Lyu. 2017. Drain: An online log parsing approach with fixed depth tree. In Proceedings of the 2017 IEEE International Conference on Web Services (ICWS). IEEE, 33–40. https://doi.org/10.1109/ICWS.2017.13 (Bibliographic data confirmed from index records; full text not read.)
  11. Graeme Horsman. 2019. Tool testing and reliability issues in the field of digital forensics. Digital Investigation 28 (2019), 163–175. https://doi.org/10.1016/j.diin.2019.01.009 (Bibliographic data confirmed from index records; full text not read.)
  12. Bret Jordan, Rich Piazza, and Trey Darley (Eds.). 2021. STIX Version 2.1. OASIS Standard, 10 June 2021. https://docs.oasis-open.org/cti/stix/v2.1/os/stix-v2.1-os.html
  13. Leonid Libkin. 2016. SQL's three-valued logic and certain answers. ACM Transactions on Database Systems 41, 1 (2016). https://doi.org/10.1145/2877206 (Bibliographic data confirmed from index records.)
  14. Mandiant. [n. d.]. macos-UnifiedLogs: A cross platform parser for Apple UnifiedLogs. GitHub repository (Apache-2.0). Release v0.6.0 used. Retrieved October 27, 2026 from https://github.com/mandiant/macos-UnifiedLogs
  15. William M. McKeeman. 1998. Differential testing for software. Digital Technical Journal 10, 1 (1998), 100–107. (Bibliographic data confirmed from index records; full text not read.)
  16. Open Policy Agent. [n. d.]. Policy Language. OPA documentation. Retrieved October 27, 2026 from https://www.openpolicyagent.org/docs/policy-language
  17. osquery. [n. d.]. osquery. GitHub repository. Retrieved October 27, 2026 from https://github.com/osquery/osquery
  18. Python Software Foundation. [n. d.]. Built-in Types: str.casefold. Python 3 documentation. Retrieved October 27, 2026 from https://docs.python.org/3/library/stdtypes.html
  19. Len Sassaman, Meredith L. Patterson, Sergey Bratus, and Michael E. Locasto. 2013. Security applications of formal language theory. IEEE Systems Journal 7, 3 (2013), 489–500. https://doi.org/10.1109/JSYST.2012.2222000 (Bibliographic data confirmed from index records; full text not read.)
  20. SigmaHQ. [n. d.]. Rules. Sigma Detection Format documentation. Retrieved October 27, 2026 from https://sigmahq.io/docs/basics/rules.html
  21. SQLite. [n. d.]. SQL Language Expressions. Retrieved October 27, 2026 from https://www.sqlite.org/lang_expr.html
  22. SQLite. [n. d.]. Limits in SQLite. Retrieved October 27, 2026 from https://www.sqlite.org/limits.html
  23. The Unicode Consortium. 2026. Unicode Standard Annex #15: Unicode Normalization Forms. Revision 58, Unicode 18.0.0, 2026-08-12. Retrieved October 27, 2026 from https://www.unicode.org/reports/tr15/
  24. Velociraptor. [n. d.]. VQL. Velociraptor documentation. Retrieved October 27, 2026 from https://docs.velociraptor.app/docs/vql/
  25. VirusTotal. [n. d.]. Writing YARA rules. YARA documentation. Retrieved October 27, 2026 from https://yara.readthedocs.io/en/stable/writingrules.html (Content confirmed through search results of the versioned documentation; page not fetched directly.)
  26. Yamato Security. [n. d.]. Hayabusa. GitHub repository. Retrieved October 27, 2026 from https://github.com/Yamato-Security/hayabusa
  27. Jieming Zhu, Shilin He, Jinyang Liu, Pinjia He, Qi Xie, Zibin Zheng, and Michael R. Lyu. 2019. Tools and benchmarks for automated log parsing. In Proceedings of the 41st International Conference on Software Engineering: Software Engineering in Practice (ICSE-SEIP 2019). IEEE/ACM, 121–130. Preprint: arXiv:1811.03509

Cite and download

Cite as

Daniel Addai, Kingsley Albert Egyir, Sanders Larweh Tetteh, and Selorm Adjabeng. 2026. Extracting Event Categories from Multi-Million-Line Mobile Logs with Predicate Rules: Design Rationale and a Repeatable Rule-Construction Method. Lab Technical Report LAB-TR-0020, version 1.0. SectraTEK Lab, Prudential Associates. Preprint, not peer reviewed. https://lab.sectratek.com/research/reports/lab-tr-0020/

Citation guide

No DOI has been assigned yet. The PDF of this report is produced from this page.

lab-tr-0020.bib
@techreport{addai2026labtr0020,
  author      = {Addai, Daniel and Egyir, Kingsley Albert and Tetteh, Sanders Larweh and Adjabeng, Selorm},
  title       = {Extracting Event Categories from Multi-Million-Line Mobile Logs with Predicate Rules: Design Rationale and a Repeatable Rule-Construction Method},
  institution = {SectraTEK Lab, Prudential Associates},
  type        = {Lab Technical Report},
  number      = {LAB-TR-0020},
  year        = {2026},
  howpublished = {https://lab.sectratek.com/research/reports/lab-tr-0020/},
  note        = {Version 1.0, preprint, not peer reviewed. CC BY 4.0}
}

Licence: CC BY 4.0. The print layout of this page is set for US Letter in two columns.

Versions and corrections

VersionDateChange
1.02026-10-28First release.

Corrections: none so far. Errors can be reported through the corrections page.