Skip to main content

Observability

IterableData provides comprehensive observability features for monitoring progress and performance of data conversions and pipelines. This includes progress callbacks, built-in progress bars, and standardized metrics objects.

Overview​

Observability features enable you to:

  • Track progress in real-time during long-running operations
  • Access structured metrics for programmatic workflows
  • Integrate with monitoring systems and CI/CD pipelines
  • Monitor performance and throughput

Progress Callbacks​

Progress callbacks allow you to receive real-time updates during conversion or pipeline execution.

Conversion Progress Callbacks​

from iterable.convert import convert

def progress_callback(stats):
"""Called periodically during conversion"""
print(f"Rows read: {stats['rows_read']}, "
f"Rows written: {stats['rows_written']}, "
f"Elapsed: {stats['elapsed']:.2f}s")

if stats['estimated_total']:
percent = (stats['rows_read'] / stats['estimated_total']) * 100
print(f"Progress: {percent:.1f}%")

result = convert(
'input.csv',
'output.parquet',
progress=progress_callback,
progress_interval=500 # Call callback every 500 rows (default: 1000)
)

The progress callback receives a dictionary with:

  • rows_read: Number of rows read from source
  • rows_written: Number of rows written to destination
  • elapsed: Elapsed time in seconds
  • estimated_total: Estimated total rows (if available, otherwise None)
  • bytes_read: Number of bytes read (if available, otherwise None)
  • bytes_written: Number of bytes written (if available, otherwise None)
  • percent_complete: Percentage complete (0-100) if estimated_total is available
  • estimated_time_remaining: Estimated time remaining in seconds (if calculable, otherwise None)

Pipeline Progress Callbacks​

from iterable import open_iterable
from iterable.pipeline import pipeline

def progress_callback(stats):
"""Called periodically during pipeline execution"""
print(f"Rows processed: {stats['rows_processed']}, "
f"Elapsed: {stats['elapsed']:.2f}s")

if stats.get('throughput'):
print(f"Throughput: {stats['throughput']:.0f} rows/second")

with open_iterable('input.csv') as source:
with open_iterable('output.jsonl', mode='w') as destination:
def process_func(record, state):
return record

result = pipeline(
source=source,
destination=destination,
process_func=process_func,
progress=progress_callback
)

The progress callback receives a dictionary with:

  • rows_processed: Number of rows processed
  • elapsed: Elapsed time in seconds
  • throughput: Rows per second (if calculable)
  • rec_count: Total record count
  • exceptions: Number of exceptions encountered
  • nulls: Number of null results

Built-in Progress Bars​

The convert() function supports built-in progress bars using tqdm when available.

from iterable.convert import convert

# Show progress bar during conversion
result = convert(
'input.jsonl.gz',
'output.parquet',
show_progress=True
)

The progress bar:

  • Shows rows processed and estimated time remaining
  • Works automatically with tqdm if installed
  • Gracefully falls back if tqdm is not available (no error)
  • Respects the silent parameter (no progress bar if silent=True)

Conversion Metrics​

The convert() function returns a ConversionResult object with structured metrics.

ConversionResult​

from iterable.convert import convert
from iterable.types import ConversionResult

result: ConversionResult = convert('input.csv', 'output.jsonl')

# Access metrics
print(f"Rows in: {result.rows_in}")
print(f"Rows out: {result.rows_out}")
print(f"Elapsed: {result.elapsed_seconds:.2f}s")
print(f"Bytes read: {result.bytes_read}")
print(f"Bytes written: {result.bytes_written}")

# Check for errors
if result.errors:
print(f"Encountered {len(result.errors)} errors")
for error in result.errors:
print(f" - {error}")

ConversionResult Attributes​

  • rows_in (int): Total number of rows read from source
  • rows_out (int): Total number of rows written to destination
  • elapsed_seconds (float): Total elapsed time in seconds
  • bytes_read (int | None): Number of bytes read (if available)
  • bytes_written (int | None): Number of bytes written (if available)
  • errors (list[Exception]): List of errors encountered (empty if none)

Pipeline Metrics​

The pipeline() function returns a PipelineResult object with structured metrics.

PipelineResult​

from iterable import open_iterable
from iterable.pipeline import pipeline
from iterable.types import PipelineResult

with open_iterable('input.csv') as source:
with open_iterable('output.jsonl', mode='w') as destination:
def process_func(record, state):
return record

result: PipelineResult = pipeline(
source=source,
destination=destination,
process_func=process_func
)

# Access metrics using attributes
print(f"Rows processed: {result.rows_processed}")
print(f"Elapsed: {result.elapsed_seconds:.2f}s")
print(f"Throughput: {result.throughput:.0f} rows/second")
print(f"Exceptions: {result.exceptions}")

# Or use dictionary-style access (backward compatible)
print(f"Record count: {result['rec_count']}")
print(f"Duration: {result['duration']:.2f}s")

PipelineResult Attributes​

  • rows_processed (int): Total number of rows processed
  • elapsed_seconds (float): Total elapsed time in seconds
  • exceptions (int): Number of exceptions encountered
  • nulls (int): Number of null results
  • rec_count (int): Total record count (alias for rows_processed)
  • time_start (float | None): Start time timestamp
  • time_end (float | None): End time timestamp
  • duration (float | None): Duration in seconds (alias for elapsed_seconds)
  • throughput (float | None): Rows per second (property, calculated)

Backward Compatibility​

PipelineResult supports both attribute access and dictionary-style access for backward compatibility:

result = pipeline(...)

# New style (attribute access)
rows = result.rows_processed

# Old style (dictionary access) - still works
rows = result["rec_count"]

Integration Examples​

CI/CD Integration​

from iterable.convert import convert

result = convert('input.csv', 'output.parquet')

# Log metrics for CI/CD
print(f"::set-output name=rows_in::{result.rows_in}")
print(f"::set-output name=rows_out::{result.rows_out}")
print(f"::set-output name=elapsed::{result.elapsed_seconds}")

# Fail build if errors occurred
if result.errors:
raise Exception(f"Conversion failed with {len(result.errors)} errors")

Monitoring Integration​

from iterable.convert import convert
import time

def send_metrics(stats):
"""Send metrics to monitoring system"""
# Example: Send to Prometheus, Datadog, etc.
metrics.gauge('conversion.rows_read', stats['rows_read'])
metrics.gauge('conversion.rows_written', stats['rows_written'])
metrics.gauge('conversion.elapsed', stats['elapsed'])

result = convert(
'input.jsonl',
'output.parquet',
progress=send_metrics
)

# Send final metrics
metrics.gauge('conversion.total_rows', result.rows_out)
metrics.gauge('conversion.total_time', result.elapsed_seconds)

Custom Progress Reporting​

from iterable.convert import convert
from datetime import datetime

class ProgressReporter:
def __init__(self):
self.start_time = datetime.now()
self.updates = []

def __call__(self, stats):
"""Progress callback"""
update = {
'timestamp': datetime.now(),
'rows_read': stats['rows_read'],
'rows_written': stats['rows_written'],
'elapsed': stats['elapsed']
}
self.updates.append(update)

# Print formatted progress
elapsed_str = f"{stats['elapsed']:.1f}s"
print(f"[{elapsed_str}] {stats['rows_read']} read, "
f"{stats['rows_written']} written")

reporter = ProgressReporter()
result = convert('input.csv', 'output.jsonl', progress=reporter)

# Analyze progress updates
print(f"Total updates: {len(reporter.updates)}")
print(f"Average rows per second: "
f"{result.rows_out / result.elapsed_seconds:.0f}")

Configurable Progress Interval​

By default, progress callbacks are invoked every 1000 rows. You can customize this interval using the progress_interval parameter:

from iterable.convert import convert

def progress_callback(stats):
print(f"Processed {stats['rows_read']} rows")

# Call callback every 500 rows for more frequent updates
result = convert(
'input.csv',
'output.parquet',
progress=progress_callback,
progress_interval=500
)

# Call callback every 5000 rows for less frequent updates (reduce overhead)
result = convert(
'large_file.csv',
'output.parquet',
progress=progress_callback,
progress_interval=5000
)

The progress_interval parameter is available in:

  • convert() - Controls callback frequency during conversion
  • pipeline() - Controls callback frequency during pipeline execution
  • bulk_convert() - Passed to each file conversion

Benefits:

  • Fine-grained control for fast operations (smaller intervals)
  • Reduced overhead for very slow operations (larger intervals)
  • Better performance tuning based on your use case

Progress Tracking for Direct Iteration​

For direct iteration over iterables (not using convert() or pipeline()), use the with_progress() helper function:

from iterable import open_iterable
from iterable.helpers.progress import with_progress

def progress_callback(stats):
print(f"Processed {stats['rows_read']} rows in {stats['elapsed']:.2f}s")
if stats.get('throughput'):
print(f"Throughput: {stats['throughput']:.0f} rows/second")

with open_iterable('large_file.csv') as source:
for row in with_progress(source, callback=progress_callback, interval=1000):
# Process each row
process(row)

The with_progress() helper:

  • Wraps any iterable with progress tracking
  • Provides stats: rows_read, elapsed, throughput
  • Configurable interval (default: 1000 rows)
  • Handles callback errors gracefully (won't break iteration)

Use cases:

  • Processing large files row-by-row
  • Custom data transformations
  • Filtering or validation operations
  • Any scenario where you need progress tracking outside of convert() or pipeline()

Error Handling in Callbacks​

Progress callbacks are designed to be non-blocking. If a callback raises an exception, it's logged but doesn't stop the conversion or pipeline:

def progress_callback(stats):
# This error won't stop the conversion
if stats['rows_read'] > 1000:
raise ValueError("Test error")

result = convert('input.csv', 'output.jsonl', progress=progress_callback)
# Conversion continues even if callback raises an exception

Structured Logging​

IterableData provides structured logging capabilities with JSON and human-readable output formats, enabling better log analysis, filtering, and integration with log aggregation systems.

Overview​

Structured logging formats log entries as structured data (JSON) rather than plain text, making it easier to:

  • Parse and analyze logs programmatically
  • Filter logs by specific fields
  • Integrate with log aggregation systems (ELK, Splunk, Datadog, etc.)
  • Track operations with operation IDs and correlation IDs
  • Query logs efficiently

Configuration​

Programmatic Configuration​

from iterable.helpers.structured_logging import configure_structured_logging
import logging

# Enable JSON structured logging
configure_structured_logging(
format="json",
level=logging.INFO
)

# Enable human-readable structured logging
configure_structured_logging(
format="human",
level=logging.DEBUG
)

# Write logs to file
configure_structured_logging(
format="json",
level=logging.INFO,
output="iterabledata.log"
)

Environment Variable Configuration​

You can enable structured logging via environment variables:

# Enable structured logging (JSON format)
export ITERABLEDATA_STRUCTURED_LOGGING=1

# Or specify format explicitly
export ITERABLEDATA_STRUCTURED_LOGGING=1
export ITERABLEDATA_LOG_FORMAT=json # or "human"

Environment Variables:

  • ITERABLEDATA_STRUCTURED_LOGGING: Set to 1, true, or yes to enable
  • ITERABLEDATA_LOG_FORMAT: json (default) or human

Log Formats​

JSON Format​

JSON format produces machine-readable logs suitable for log aggregation systems:

{
"timestamp": "2026-01-28T10:30:45.123456+00:00",
"level": "INFO",
"logger": "iterable.detect",
"message": "Detected format: csv",
"module": "detect",
"function": "detect_file_type",
"line": 245,
"operation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"correlation_id": "corr-12345",
"format_id": "csv",
"confidence": 1.0,
"detection_method": "filename"
}

Human-Readable Format​

Human-readable format produces developer-friendly logs:

[INFO] iterable.detect detect_file_type:245 op_id=a1b2c3d4 - Detected format: csv (format_id=csv, confidence=1.0, detection_method=filename)

Operation Tracking​

Structured logging supports operation tracking with operation IDs and correlation IDs:

from iterable.helpers.structured_logging import OperationContext

# Track a conversion operation
with OperationContext("conversion", source_file="input.csv", dest_file="output.parquet"):
convert('input.csv', 'output.parquet')
# All logs within this context include the operation_id

Operation Context Benefits:

  • Operation IDs: Unique identifier for each operation (UUID)
  • Correlation IDs: Link related operations together
  • Context propagation: Automatically included in all log entries
  • Traceability: Track operations across multiple log entries

Log Event Types​

Structured logging uses standard event types for categorization:

  • format_detection: Format detection operations
  • file_io: File I/O operations
  • parsing: Data parsing operations
  • conversion: Format conversion operations
  • pipeline: Pipeline processing operations
  • error: Error events
  • performance: Performance metrics
  • validation: Data validation operations

Integration Examples​

ELK Stack (Elasticsearch, Logstash, Kibana)​

from iterable.helpers.structured_logging import configure_structured_logging
import logging

# Configure JSON logging for ELK
configure_structured_logging(
format="json",
level=logging.INFO,
output="/var/log/iterabledata/iterabledata.log"
)

# Logstash can parse JSON logs directly
# Elasticsearch indexes structured fields for easy querying

Datadog Integration​

import logging
from iterable.helpers.structured_logging import configure_structured_logging

# Configure structured logging
configure_structured_logging(format="json", level=logging.INFO)

# Datadog agent automatically parses JSON logs
# Fields are available as tags/facets in Datadog

Custom Log Handler​

import logging
from iterable.helpers.structured_logging import configure_structured_logging, StructuredJSONFormatter

# Create custom handler (e.g., send to API)
class APIHandler(logging.Handler):
def emit(self, record):
log_data = self.format(record)
# Send to your API
send_to_api(json.loads(log_data))

# Configure with custom handler
handler = APIHandler()
handler.setFormatter(StructuredJSONFormatter())
configure_structured_logging(handler=handler, level=logging.INFO)

Checking Logging Status​

from iterable.helpers.structured_logging import is_structured_logging_enabled

if is_structured_logging_enabled():
print("Structured logging is enabled")
else:
print("Structured logging is disabled")

Best Practices​

  1. Use JSON format for production: JSON format is better for log aggregation systems
  2. Use human-readable for development: Human-readable format is easier to read during development
  3. Set appropriate log levels: Use DEBUG for development, INFO for production
  4. Use operation context: Wrap operations in OperationContext for better traceability
  5. Integrate with monitoring: Send structured logs to your monitoring/logging infrastructure

Best Practices​

  1. Use progress callbacks for long operations: Progress callbacks are most useful for conversions or pipelines that take significant time
  2. Keep callbacks lightweight: Avoid heavy operations in progress callbacks to maintain performance
  3. Handle errors gracefully: Progress callbacks should handle errors internally to avoid disrupting the main operation
  4. Use metrics for automation: Access ConversionResult and PipelineResult metrics programmatically for CI/CD and monitoring
  5. Combine with logging: Use progress callbacks alongside structured logging for comprehensive observability
  6. Enable structured logging: Use structured logging for better log analysis and integration with monitoring systems