Intermediate File Generator
March 19, 2026 · View on GitHub
Overview
The Intermediate File Generator (generators/intermediate_files.py) is a critical component in the ECS build pipeline. It transforms processed schemas into standardized intermediate representations that serve as the foundation for all downstream artifact generation.
Purpose
This generator bridges the gap between schema processing and artifact generation by creating two normalized formats:
- Flat Format (
ecs_flat.yml) - Single-level field dictionary - Nested Format (
ecs_nested.yml) - Hierarchical fieldset organization
These intermediate files provide:
- Stable Interface: Consistent data structure for all generators
- Separation of Concerns: Schema processing logic separate from artifact generation
- Debugging Aid: Human-readable checkpoints in the pipeline
- Multiple Consumers: CSV, Elasticsearch templates, Beats, documentation
Architecture
Pipeline Position
┌─────────────────────────────────────────────────────────────────┐
│ Schema Processing │
│ │
│ 1. loader.py - Load YAML schemas from files │
│ 2. cleaner.py - Normalize and validate │
│ 3. finalizer.py - Apply transformations │
│ 4. subset_filter.py - Optional filtering │
│ 5. exclude_filter.py - Optional exclusions │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ intermediate_files.generate() │
│ [THIS MODULE] │
│ │
│ Input: Dict[str, FieldEntry] (processed schemas) │
│ │
│ ┌───────────────────────┐ ┌───────────────────────┐ │
│ │ generate_flat_fields()│ │generate_nested_fields()│ │
│ │ │ │ │ │
│ │ • Filter non-root │ │ • Keep all fieldsets │ │
│ │ • Flatten hierarchy │ │ • Group by fieldset │ │
│ │ • Index by flat_name │ │ • Preserve metadata │ │
│ └───────────┬───────────┘ └──────────┬─────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ecs_flat.yml (850 fields) ecs_nested.yml (45 fieldsets) │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Artifact Generators │
│ │
│ • CSV Generator - Uses ecs_flat.yml │
│ • Elasticsearch - Uses ecs_nested.yml │
│ • Beats Generator - Uses ecs_nested.yml │
│ • Markdown Generator - Uses ecs_nested.yml │
└─────────────────────────────────────────────────────────────────┘
Data Flow
Input: Processed Schemas
↓
{
'http': {
'field_details': {...},
'schema_details': {...},
'fields': {
'request': {
'fields': {
'method': {
'field_details': {
'flat_name': 'http.request.method',
'type': 'keyword',
...
}
}
}
}
}
}
}
↓
├─── generate_flat_fields() ───→ Flat Format
│ {
│ 'http.request.method': {
│ 'name': 'method',
│ 'type': 'keyword',
│ ...
│ }
│ }
│
└─── generate_nested_fields() ──→ Nested Format
{
'http': {
'name': 'http',
'title': 'HTTP',
'fields': {
'http.request.method': {...}
}
}
}
File Formats
Flat Format (ecs_flat.yml)
Purpose: Quick lookup and iteration over all fields
Structure:
# Single-level dictionary, fields indexed by full dotted name
http.request.method:
name: method
flat_name: http.request.method
type: keyword
description: HTTP request method
example: GET
level: extended
normalize:
- array
otel:
- relation: match
stability: stable
http.response.status_code:
name: status_code
flat_name: http.response.status_code
type: long
description: HTTP response status code
example: 404
level: extended
Characteristics:
- Keys: Full dotted field names (e.g.,
http.request.method) - Values: Complete field definitions
- Excludes: Non-root reusable fieldsets (top_level=false)
- Excludes: Intermediate structural fields
- Count: ~850 fields in standard ECS
Use Cases:
- CSV generation (one row per field)
- Simple field lookups by name
- Validation scripts
- Field counting and statistics
Nested Format (ecs_nested.yml)
Purpose: Preserve logical grouping and fieldset metadata
Structure:
# Top-level: fieldsets
http:
name: http
title: HTTP
group: 2
description: Fields related to HTTP activity
type: group
reusable:
top_level: true
expected:
- client
- server
reused_here:
- full: http.request
short: request
schema_name: http.request
fields:
# Flat dictionary of all fields in this fieldset
http.request.method:
name: method
flat_name: http.request.method
type: keyword
description: HTTP request method
...
http.response.status_code:
name: status_code
flat_name: http.response.status_code
type: long
...
user:
name: user
title: User
group: 2
description: User fields
reusable:
top_level: true
expected:
- client
- destination
- server
- source
fields:
user.email:
name: email
...
Characteristics:
- Keys: Fieldset names (e.g.,
http,user,process) - Values: Fieldset metadata + fields dictionary
- Includes: All fieldsets (even top_level=false)
- Fields: Stored in nested
fieldsdict (still flat, not hierarchical) - Count: ~45 fieldsets in standard ECS
Use Cases:
- Documentation generation (one page per fieldset)
- Elasticsearch templates (field grouping)
- Beats configuration
- Understanding field relationships
Key Concepts
Top-Level vs. Non-Root Reusable Fieldsets
Some fieldsets are designed ONLY to be reused in specific locations:
Non-Root Reusable (top_level=false):
# geo fieldset - only valid under client.geo, source.geo, etc.
geo:
reusable:
top_level: false # Never appears as geo.* at root
expected:
- client.geo
- destination.geo
- source.geo
Root Reusable (top_level=true):
# user fieldset - valid at root AND reused locations
user:
reusable:
top_level: true # Can appear as user.* at root
expected:
- client.user
- destination.user
- source.user
Filtering Behavior:
- Flat format: Excludes top_level=false fieldsets
- Nested format: Includes all fieldsets (consumers decide)
Intermediate Fields
Some fields exist only for structural purposes:
# Intermediate field - creates hierarchy but isn't a real field
http.request:
intermediate: true # Not a field itself
fields:
method: {...} # Actual field: http.request.method
body: {...} # Actual field: http.request.body
These are excluded from intermediate files as they don't represent actual data.
Internal Attributes
Attributes removed from final output:
node_name: Internal tree traversal identifierintermediate: Flag for structural-only fieldsdashed_name: Alternative name format (not needed in output)
Usage Examples
See README.md for generator invocation commands.
Programmatic Usage
from schema import loader, cleaner, finalizer
from generators.intermediate_files import generate
# Process schemas
fields = loader.load_schemas()
cleaner.clean(fields)
finalizer.finalize(fields)
# Generate intermediate files
nested, flat = generate(
fields=fields,
out_dir='generated/ecs',
default_dirs=True # Also save raw ecs.yml
)
# Use the returned structures
print(f"Total fields: {len(flat)}")
print(f"Total fieldsets: {len(nested)}")
# Access specific field
method_field = flat['http.request.method']
print(f"Type: {method_field['type']}")
# Access fieldset
http_fieldset = nested['http']
print(f"Title: {http_fieldset['title']}")
print(f"Fields in HTTP: {len(http_fieldset['fields'])}")
Reading Generated Files
import yaml
# Load flat format
with open('generated/ecs/ecs_flat.yml') as f:
flat = yaml.safe_load(f)
# Iterate all fields
for field_name, field_def in flat.items():
print(f"{field_name}: {field_def['type']}")
# Load nested format
with open('generated/ecs/ecs_nested.yml') as f:
nested = yaml.safe_load(f)
# Process by fieldset
for fieldset_name, fieldset in nested.items():
print(f"\n{fieldset['title']} ({fieldset_name})")
for field_name in fieldset['fields']:
print(f" - {field_name}")
Making Changes
Adding New Field Attributes
If you add a new attribute to field definitions:
- Update schema files (in
schemas/*.yml) - Update type definitions (in
ecs_types/schema_fields.py) - No changes needed here - attributes pass through automatically
- Update downstream consumers if they need to use the new attribute
Example: Adding a sensitivity attribute
# In schema
- name: password
type: keyword
sensitivity: high # NEW attribute
# Automatically appears in both formats:
# ecs_flat.yml
user.password:
name: password
type: keyword
sensitivity: high # Passed through
# ecs_nested.yml
user:
fields:
user.password:
sensitivity: high # Passed through
Filtering Additional Attributes
To remove an attribute from intermediate files:
def remove_internal_attributes(field_details: Field) -> None:
"""Remove internal-only attributes."""
field_details.pop('node_name', None)
field_details.pop('intermediate', None)
field_details.pop('new_internal_attr', None) # Add this
Changing Flat Format Filtering
To change which fields appear in the flat format:
def generate_flat_fields(fields: Dict[str, FieldEntry]) -> Dict[str, Field]:
"""Generate flat field representation."""
filtered: Dict[str, FieldEntry] = remove_non_root_reusables(fields)
# Add additional filtering
filtered = remove_deprecated_fields(filtered) # NEW
flattened: Dict[str, Field] = {}
visitor.visit_fields_with_memo(filtered, accumulate_field, flattened)
return flattened
Modifying Nested Format Structure
To change fieldset-level attributes:
def generate_nested_fields(fields: Dict[str, FieldEntry]) -> Dict[str, FieldNestedEntry]:
"""Generate nested fieldset representation."""
nested: Dict[str, FieldNestedEntry] = {}
for (name, details) in fields.items():
fieldset_details = {
**copy.deepcopy(details['field_details']),
**copy.deepcopy(details['schema_details'])
}
# Add custom processing
if 'beta' in fieldset_details:
fieldset_details['stability'] = 'beta' # NEW
# ... rest of processing ...
Troubleshooting
Common Issues
Missing fields in flat format
Symptom: Field appears in schema but not in ecs_flat.yml
Possible causes:
-
Field is in a fieldset with
top_level: false- Check: Look at fieldset's
reusable.top_levelsetting - Solution: If field should be at root, set
top_level: true
- Check: Look at fieldset's
-
Field is marked as
intermediate: true- Check: Look for
intermediateattribute in schema - Solution: Remove if field should be included
- Check: Look for
-
Field is in a filtered subset
- Check: Are you using
--subsetor--excludeflags? - Solution: Adjust filtering or run without filters
- Check: Are you using
Fieldset missing from nested format
Symptom: Fieldset defined in schema but not in ecs_nested.yml
Unlikely: The nested format includes all fieldsets by design.
Check:
- Verify fieldset is properly defined in schema
- Check for schema validation errors earlier in pipeline
- Ensure schema file is in the loaded directory
Unexpected attributes in output
Symptom: Internal attributes appearing in intermediate files
Solution: Add to remove_internal_attributes():
def remove_internal_attributes(field_details: Field) -> None:
field_details.pop('node_name', None)
field_details.pop('intermediate', None)
field_details.pop('unwanted_attr', None) # Add this
Debugging Tips
- Use
default_dirs=Trueto generateecs.ymlwith raw processed schemas - Compare outputs:
diff ecs_flat_old.yml ecs_flat_new.yml - Count fields with
len(yaml.safe_load(open('ecs_flat.yml')))