Skip to main content

Usage Ingestion

Usage ingestion collects query logs from databases, parses them, aggregates usage counts per table, and publishes the results to OpenMetadata. It answers questions like “which tables are most queried?” and “who is using this table?”. Unlike metadata ingestion (Source → Sink), usage follows a 5-stage pipeline: Source → Processor → Stage → BulkSink.

Pipeline Overview

Data Model Flow

Each stage transforms the data through a chain of models:

Stage 1: Query Log Collection

UsageSource (source/database/usage_source.py) extends QueryParserSource and fetches raw query logs. Two modes:

Database Query Mode

Each database connector provides SQL to fetch from system views: Configuration controls:
  • queryLogDuration — how many days back to look (default: 1)
  • resultLimit — max queries to fetch (default: 100,000)
  • filterCondition — custom SQL WHERE clause

File Mode

Set queryLogFilePath to a CSV file containing query logs. The file is parsed into TableQuery objects.

Stage 2: SQL Parsing

QueryParserProcessor (processor/query_parser.py) calls parse_sql_statement() for each query.

Parsing Pipeline

The LineageParser (lineage/parser.py) is shared with lineage ingestion — usage ingestion uses the table extraction capabilities, while lineage ingestion additionally uses the source/target direction analysis.

What Gets Extracted

From a query like:
The parser extracts:
  • tables: ["orders", "customers"]
  • joins: {"orders": [{"columnName": "customer_id", "joinedWith": {"table": "customers", "column": "id"}}]}
  • query_type: "SELECT"

Stage 3: Aggregation

TableUsageStage (stage/table_usage.py) groups parsed queries and writes to temporary files.

Aggregation Logic

Staging Files

Two types of JSON Lines files are written to the staging directory:
  • Usage files: {serviceName}_{date} — one TableUsageCount per line
  • Cost files: {serviceName}_{date}_query — one QueryCostWrapper per line
This staged approach keeps memory usage bounded — records are written incrementally rather than held in memory.

Stage 4: Publishing

MetadataUsageBulkSink (bulksink/metadata_usage.py) reads the staged files and publishes to OpenMetadata.

For Each Table Usage Record

  1. Resolve table entity — look up table by FQN via ES search + API fallback
  2. Publish usage countPOST /api/v1/usage/table/{id} with UsageRequest(date, count)
  3. Publish joins — send TableJoins with column-level join information (powers “Frequently Joined With” in the UI)
  4. Create Query entities — create/link Query entities to the table
  5. Update life cycle — infer created/modified/accessed dates from query types:
    • SELECT → accessed
    • INSERT/UPDATE → modified
    • CREATE → created

After All Records

  • Compute percentilesPOST /api/v1/usage/compute.percentile/{entityType}/{date} ranks tables by usage

Query Cost Records

For databases that report cost (Snowflake, BigQuery):
  • Creates QueryCostRecord entities linked to queries by hash
  • Tracks: total cost, execution count, total duration

Life Cycle Integration

LifeCycleQueryMixin (source/database/life_cycle_query_mixin.py) infers table lifecycle from query patterns:

Configuration

Performance

  • Query hash caching — 5,000-entry LRU cache for query lookups
  • Parser timeout — 30 seconds max per query
  • Memory limits — 100MB max per query parsing session
  • Staged processing — files written incrementally, not held in memory
  • Batch size control — configurable resultLimit per database

Key Design Patterns

Key Files Quick Reference

All paths above are relative to ingestion/src/metadata/ingestion/. For example, processor/query_parser.py means ingestion/src/metadata/ingestion/processor/query_parser.py.