Skip to content

Commit 363a4f8

Browse files
authored
Merge pull request #30 from Point72/feat/io-source-profiling
feat: OpenTelemetry instrumentation for custom IO sources
2 parents 0ef48ea + b162de5 commit 363a4f8

18 files changed

Lines changed: 1064 additions & 25 deletions

polars_io_tools/io_sources/concat_named.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def concat_named(
1818
identifier_cols: list[str | tuple[str, pl.DataType]],
1919
*,
2020
log_explain: bool = False,
21+
description: str | None = None,
2122
**kwargs: Any,
2223
) -> pl.LazyFrame:
2324
"""
@@ -47,6 +48,8 @@ def concat_named(
4748
4849
log_explain (bool, default False): If True, logs the LazyFrame execution plan for debugging purposes.
4950
51+
description: Optional free-form description of this source instance, attached to its OpenTelemetry span (``explain_detail``).
52+
5053
**kwargs (Any): Additional arguments passed to `pl.concat()` for concatenation.
5154
5255
Returns:
@@ -177,4 +180,4 @@ def source_gen(
177180
err_msg += f"\n\nWhile running the above, received error: {e.__class__.__name__}:{e}"
178181
raise RuntimeError(err_msg) from e
179182

180-
return register_io_source_with_is_pure(source_gen, schema=schema)
183+
return register_io_source_with_is_pure(source_gen, schema=schema, explain_detail=description)

polars_io_tools/io_sources/delta_io.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,7 @@ def scan_delta(
578578
rechunk: bool | None = None,
579579
aws_profile: str | None = None,
580580
pushdown_predicate_deltalake: bool = True,
581+
description: str | None = None,
581582
) -> pl.LazyFrame:
582583
"""
583584
Lazily read from a Delta lake table with logical type translation.
@@ -635,6 +636,7 @@ def scan_delta(
635636
reading file metadata. This can significantly reduce I/O on partitioned
636637
tables. When ``False``, falls back to standard ``pl.scan_delta`` behavior
637638
without partition pruning.
639+
description: Optional free-form description of this source instance, attached to its OpenTelemetry span (``explain_detail``).
638640
639641
Returns:
640642
LazyFrame
@@ -771,4 +773,4 @@ def source_generator(
771773

772774
yield from collect_lf_in_io_source(lf, batch_size)
773775

774-
return register_io_source_with_is_pure(source_generator, schema=exposed_schema)
776+
return register_io_source_with_is_pure(source_generator, schema=exposed_schema, explain_detail=description)

polars_io_tools/io_sources/join.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def _dummy_source(
8383
df = df.select(with_columns)
8484
yield df
8585

86-
lf = register_io_source_with_is_pure(_dummy_source, schema=schema)
86+
lf = register_io_source_with_is_pure(_dummy_source, schema=schema, explain_name="filtered_join.predicate_rename")
8787
# We apply the rename here to have polars change the column names for us
8888
# NOTE: we are performing the rename right to left here. This might seem a bit
8989
# counterintuitive, but we are renaming the right columns to the left columns.
@@ -106,6 +106,7 @@ def filtered_join(
106106
right_on: str | list[str] | None = None,
107107
nulls_equal: bool = False,
108108
log_explain: bool = False,
109+
description: str | None = None,
109110
**join_kwargs,
110111
) -> pl.LazyFrame:
111112
"""
@@ -247,7 +248,7 @@ def source_generator(
247248
err_msg += f"\n\nError: {e.__class__.__name__}:{e}"
248249
raise RuntimeError(err_msg) from e
249250

250-
return register_io_source_with_is_pure(source_generator, schema=schema)
251+
return register_io_source_with_is_pure(source_generator, schema=schema, explain_detail=description)
251252

252253

253254
def filtered_join_asof(
@@ -263,6 +264,7 @@ def filtered_join_asof(
263264
strategy: Literal["backward", "forward", "nearest"] = "backward",
264265
tolerance: str | float | datetime.timedelta | None = None, # TODO: Only timedelta is supported for now
265266
log_explain: bool = True,
267+
description: str | None = None,
266268
**join_kwargs,
267269
) -> pl.LazyFrame:
268270
"""
@@ -299,6 +301,7 @@ def filtered_join_asof(
299301
tolerance (timedelta, optional): Maximum time difference allowed for a match. Currently only timedelta is supported.
300302
When specified, enables temporal range expansion optimization.
301303
log_explain (bool, default True): Whether to log detailed execution plans for debugging
304+
description: Optional free-form description of this source instance, attached to its OpenTelemetry span (``explain_detail``).
302305
**join_kwargs: Additional keyword arguments passed to the underlying join_asof operation
303306
304307
Returns:
@@ -505,7 +508,7 @@ def source_generator(
505508
err_msg += f"\n\nError: {e.__class__.__name__}:{e}"
506509
raise RuntimeError(err_msg) from e
507510

508-
return register_io_source_with_is_pure(source_generator, schema=schema)
511+
return register_io_source_with_is_pure(source_generator, schema=schema, explain_detail=description)
509512

510513

511514
def join_between(

polars_io_tools/io_sources/lazy_cache.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def cache(
139139
cache_mode: Literal["cache", "ignore", "rebuild"] = "cache",
140140
validate: bool = True,
141141
log_explain: bool = False,
142+
description: str | None = None,
142143
**kwargs,
143144
) -> pl.LazyFrame:
144145
"""
@@ -175,6 +176,7 @@ def cache(
175176
collected for the cache fill, so it adds no extra pass over the source. Set to False to
176177
skip it on hot paths where uniqueness is already guaranteed.
177178
log_explain: If True, logs the query plan when defining the function.
179+
description: Optional free-form description of this source instance, attached to its OpenTelemetry span (``explain_detail``).
178180
**kwargs: Arguments to pass to the collect() method of the input data frame (i.e. to use a different engine)
179181
180182
Notes:
@@ -446,4 +448,4 @@ def source_generator(
446448
else:
447449
yield from df.iter_slices(n_rows=batch_size)
448450

449-
return register_io_source_with_is_pure(source_generator, schema=schema, validate_schema=True)
451+
return register_io_source_with_is_pure(source_generator, schema=schema, validate_schema=True, explain_detail=description)

polars_io_tools/io_sources/lazy_cache_memory.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def cache_memory(
7272
self_or_fn: pl.LazyFrame | Callable[[], pl.LazyFrame],
7373
*,
7474
schema: pl.Schema | Callable[[], pl.Schema],
75+
description: str | None = None,
7576
) -> pl.LazyFrame:
7677
"""Collect a builder at most once into an in-memory buffer and replay it thereafter.
7778
@@ -97,6 +98,7 @@ def cache_memory(
9798
A callable defers resolution until Polars needs it, so ``collect_schema()`` on
9899
the result never forces the builder to run — useful when the schema is derived
99100
from the builder's own lazy plan.
101+
description (str | None, default None): Optional free-form description of this source instance, attached to its OpenTelemetry span (``explain_detail``).
100102
101103
Returns:
102104
pl.LazyFrame: A LazyFrame with ``schema``, backed by a generator over a one-time
@@ -237,4 +239,4 @@ def source_generator(
237239
# register_io_source_with_is_pure (unlike the plain register_io_source, which raises ComputeError
238240
# for a callable schema) accepts a zero-arg callable schema and resolves it lazily. We hand it the
239241
# memoizing ``get_schema`` so Polars and the buffer reconciliation agree on one resolved schema.
240-
return register_io_source_with_is_pure(io_source=source_generator, schema=get_schema)
242+
return register_io_source_with_is_pure(io_source=source_generator, schema=get_schema, explain_detail=description)

polars_io_tools/io_sources/lazy_cache_parquet.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1429,6 +1429,7 @@ def cache_parquet(
14291429
extra_partition_cols: str | list[str] | None = None,
14301430
schema: pl.Schema | None = None,
14311431
write_bounding_columns: list[str] | None = None,
1432+
description: str | None = None,
14321433
) -> pl.LazyFrame:
14331434
"""
14341435
Cache a LazyFrame to Parquet files with optional date-based partitioning. Supports daily, monthly, or yearly
@@ -1489,6 +1490,7 @@ def cache_parquet(
14891490
partition columns, not content), a bounded cache is typically regenerated with ``cache_mode=CacheMode.REBUILD``
14901491
so each run overwrites with the current predicate's rows; under ``CacheMode.CACHE`` a partition first written
14911492
under one predicate is not re-written for a wider one. Default None leaves write behavior unchanged.
1493+
description: Optional free-form description of this source instance, attached to its OpenTelemetry span (``explain_detail``).
14921494
14931495
Returns:
14941496
pl.LazyFrame: If the cache has all data: a LazyFrame reading from the cache.
@@ -1929,4 +1931,4 @@ def error_wrapper(e):
19291931
end = time.time()
19301932
log.debug("End: Loading data from cache at %s took %s seconds", time_unit_dir, end - start)
19311933

1932-
return register_io_source_with_is_pure(io_source=source_generator, schema=schema, validate_schema=False)
1934+
return register_io_source_with_is_pure(io_source=source_generator, schema=schema, validate_schema=False, explain_detail=description)

polars_io_tools/io_sources/lazy_clickhouse_reader.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def get_batch_reader_http(query: str, url: str, params: dict):
2525
return pa.ipc.open_stream(r.raw)
2626

2727

28-
def scan_clickhouse(query: str, url: str, params: dict, fetch_size: int = 10000):
28+
def scan_clickhouse(query: str, url: str, params: dict, fetch_size: int = 10000, description: str | None = None):
2929
# TODO: fetch_size param needs to be properly handled
3030
log.warning("fetch_size=%d is currently ignored and has no effect. Proper fetch_size support will be added in a future release.", fetch_size)
3131
dialect = "clickhouse"
@@ -102,4 +102,4 @@ def select_cols(df) -> pl.DataFrame:
102102
err_msg += f"\n\nWhile running the above, received error: {e.__class__.__name__}:{e}"
103103
raise RuntimeError(err_msg) from e
104104

105-
return register_io_source_with_is_pure(source_generator, schema=schema)
105+
return register_io_source_with_is_pure(source_generator, schema=schema, explain_detail=description)

polars_io_tools/io_sources/lazy_data_generator.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ def _register_source(
4646
mean_computer: MeanComputer,
4747
extras_schema: dict,
4848
chunk_sizes: np.ndarray | None = None,
49+
explain_name: str,
50+
explain_detail: str | None = None,
4951
) -> pl.LazyFrame:
5052
feature_cols = [f"x{i}" for i in range(n_features)]
5153
response_cols = [f"y{i}" for i in range(n_responses)]
@@ -120,7 +122,9 @@ def source_generator(
120122
log.debug("scan_synthetic: yielded %d rows; remaining_gen=%d, remaining_deliver=%d", df.height, remaining_gen, remaining_deliver)
121123
yield df
122124

123-
return register_io_source_with_is_pure(source_generator, schema=schema, is_pure=seed is not None)
125+
return register_io_source_with_is_pure(
126+
source_generator, schema=schema, is_pure=seed is not None, explain_name=explain_name, explain_detail=explain_detail
127+
)
124128

125129

126130
def _validate_common(
@@ -160,6 +164,7 @@ def scan_synthetic_regression(
160164
n_chunks: int | None = None,
161165
seed: int | None = None,
162166
fetch_size: int = 10_000,
167+
description: str | None = None,
163168
) -> pl.LazyFrame:
164169
"""
165170
A lazy source of synthetic linear-regression data ``Y = X @ B + E`` with Gaussian noise.
@@ -187,6 +192,7 @@ def scan_synthetic_regression(
187192
n_chunks: Number of contiguous chunks to split ``n_samples`` into. Required when ``chunk_key`` is set. Must satisfy ``1 <= n_chunks <= n_samples``.
188193
seed: Seed for ``np.random.default_rng``. If None, uses fresh entropy per call (and the source is registered with ``is_pure=False``).
189194
fetch_size: Default number of rows generated per batch when Polars does not provide a ``batch_size``. Must be >= 1. Defaults to 10_000.
195+
description: Optional free-form description of this source instance, attached to its OpenTelemetry span (``explain_detail``).
190196
"""
191197
_validate_common(
192198
n_features=n_features,
@@ -254,6 +260,8 @@ def mean_computer(
254260
mean_computer=mean_computer,
255261
extras_schema=extras_schema,
256262
chunk_sizes=chunk_sizes,
263+
explain_name="scan_synthetic_regression",
264+
explain_detail=description,
257265
)
258266

259267

@@ -275,6 +283,7 @@ def scan_synthetic_panel(
275283
epsilon_scale: float = 1.0,
276284
seed: int | None = None,
277285
fetch_size: int = 10_000,
286+
description: str | None = None,
278287
) -> pl.LazyFrame:
279288
"""
280289
A lazy source of synthetic panel data on a ``(date, symbol)`` grid. This generator draws independent per-row noise, uses ``x_i``/``y_i`` column names, and treats weights as a WLS variance model. Rows are yielded date-by-date so ``.set_sorted("date").group_by("date")`` streams cleanly under ``engine="streaming"``.
@@ -302,6 +311,7 @@ def scan_synthetic_panel(
302311
epsilon_scale: When ``use_weights=False``, the noise stddev for every row. When ``use_weights=True``, the *reference* stddev at ``w=1``; actual per-row noise is ``N(loc, (epsilon_scale/√w)²)``. Must be >= 0. Defaults to 1.0.
303312
seed: Seed for ``np.random.default_rng``. If None, uses fresh entropy per call (and the source is registered with ``is_pure=False``).
304313
fetch_size: Default number of rows generated per batch when Polars does not provide a ``batch_size``. Must be >= 1. Defaults to 10_000.
314+
description: Optional free-form description of this source instance, attached to its OpenTelemetry span (``explain_detail``).
305315
"""
306316
_validate_common(
307317
n_features=n_features,
@@ -436,4 +446,6 @@ def mean_computer(
436446
mean_computer=mean_computer,
437447
extras_schema=extras_schema,
438448
chunk_sizes=chunk_sizes,
449+
explain_name="scan_synthetic_panel",
450+
explain_detail=description,
439451
)

polars_io_tools/io_sources/lazy_datadog_reader.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def scan_datadog(
5353
dd_interval: int | None = None,
5454
additional_schema: dict | None = None,
5555
overwrite_schema: bool = False,
56+
description: str | None = None,
5657
) -> pl.LazyFrame:
5758
"""
5859
Return a Polars `LazyFrame` that holds the result of a Datadog
@@ -92,6 +93,7 @@ def scan_datadog(
9293
columns that you expect to be present in the response.
9394
overwrite_schema: If True, the `additional_schema` will overwrite the default schema (not
9495
add to it). You should probably avoid using this for safety reasons.
96+
description: Optional free-form description of this source instance, attached to its OpenTelemetry span (``explain_detail``).
9597
9698
Returns:
9799
pl.LazyFrame: A Polars LazyFrame
@@ -277,4 +279,4 @@ def source_generator(
277279

278280
yield df
279281

280-
return register_io_source_with_is_pure(source_generator, schema=schema)
282+
return register_io_source_with_is_pure(source_generator, schema=schema, explain_detail=description)

polars_io_tools/io_sources/lazy_debug.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
def debug(
1515
self: pl.LazyFrame,
1616
log_level: int | None = None,
17+
description: str | None = None,
1718
) -> pl.LazyFrame:
1819
"""
1920
A very simple pass-through lazy frame source to help with debugging experimentation of polars io sources and lazy frame behavior.
2021
2122
Args:
2223
self: The input data frame to cache columns of.
2324
log_level: If provided, will log at the given level. If None, will print. Defaults to None.
25+
description: Optional free-form description of this source instance, attached to its OpenTelemetry span (``explain_detail``).
2426
"""
2527
schema = self.collect_schema()
2628

@@ -53,4 +55,4 @@ def source_generator(
5355
raise RuntimeError(err_msg) from e
5456

5557
# TODO: Turn on validate_schema when this is solved: https://github.com/pola-rs/polars/issues/22110
56-
return register_io_source_with_is_pure(source_generator, schema=schema, validate_schema=False)
58+
return register_io_source_with_is_pure(source_generator, schema=schema, validate_schema=False, explain_detail=description)

0 commit comments

Comments
 (0)