Skip to content

Commit fd75549

Browse files
authored
feat: system query_metrics table uses timestamps, and improve session metrics output (#241)
## What Changed? - start_ts and end_ts in fenic fenic_system.query_metrics are datetimes with UTC timezone instead of strings (TIMESTAMPTZ in duckdb) - Unit test for above - optionally include LM and RM stats in session stop metrics printout. e.g., if any LM calls, include request count and token usage - improve formatting to show 6 decimal places if they matter, otherwise fewer decimal places. ## Testing and outputs ### query_metrics table ``` (Pdb) session.table("fenic_system.query_metrics").select("start_ts", "end_ts").show() ┌────────────────────────────────┬────────────────────────────────┐ │ start_ts ┆ end_ts │ ╞════════════════════════════════╪════════════════════════════════╡ │ 2025-10-01 10:30:47.978108 UTC ┆ 2025-10-01 10:30:47.979987 UTC │ │ 2025-10-01 10:30:47.985357 UTC ┆ 2025-10-01 10:30:47.985617 UTC │ │ 2025-10-01 10:31:22.398716 UTC ┆ 2025-10-01 10:31:22.402193 UTC │ ... ``` ``` (Pdb) pp session.table("fenic_system.query_metrics").select("start_ts", "end_ts").schema Schema(column_fields=[ColumnField(name='start_ts', data_type=_TimestampType(timezone='UTC')), ColumnField(name='end_ts', data_type=_TimestampType(timezone='UTC'))]) ``` ### Metrics after session with no LM: Session Usage Summary: App Name: test_app Session ID: a2a2d3a7-6023-40b6-bbb1-ced868d19282 Total queries executed: 4 Total execution time: 3.64ms Total rows processed: 11 Total language model cost: $0.00 Total embedding model cost: $0.00 Total cost: $0.00 ### Metrics after session with LM costs: Session Usage Summary: App Name: document_extraction Session ID: efba1fb1-3c90-45cc-8380-033fe90ca4e2 Total queries executed: 3 Total execution time: 3045.72ms Total rows processed: 15 Total language model cost: $0.000682 Total language model requests: 5 Total language model tokens: 2,960 input tokens, 0 cached input tokens, 398 output tokens Total embedding model cost: $0.00 Total cost: $0.000682
1 parent c6167d9 commit fd75549

File tree

3 files changed

+57
-11
lines changed

3 files changed

+57
-11
lines changed

src/fenic/_backends/local/session_state.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44
import uuid
5+
from decimal import ROUND_DOWN, Decimal
56
from functools import cached_property
67
from pathlib import Path
78
from typing import Optional
@@ -122,12 +123,27 @@ def _print_session_usage_summary(self):
122123
print(f" Total queries executed: {costs['query_count']}")
123124
print(f" Total execution time: {costs['total_execution_time_ms']:.2f}ms")
124125
print(f" Total rows processed: {costs['total_output_rows']:,}")
125-
print(f" Total language model cost: ${costs['total_lm_cost']:.6f}")
126-
print(f" Total embedding model cost: ${costs['total_rm_cost']:.6f}")
126+
print(f" Total language model cost: ${_format_float(costs['total_lm_cost'])}")
127+
if costs['total_lm_requests'] > 0:
128+
print(f" Total language model requests: {costs['total_lm_requests']}")
129+
print(f" Total language model tokens: {costs['total_lm_uncached_input_tokens']:,} input tokens, {costs['total_lm_cached_input_tokens']:,} cached input tokens, {costs['total_lm_output_tokens']:,} output tokens")
130+
print(f" Total embedding model cost: ${_format_float(costs['total_rm_cost'])}")
131+
if costs['total_rm_requests'] > 0:
132+
print(f" Total embedding model requests: {costs['total_rm_requests']}")
133+
print(f" Total embedding model tokens: {costs['total_rm_input_tokens']:,} input tokens")
127134
total_cost = costs['total_lm_cost'] + costs['total_rm_cost']
128-
print(f" Total cost: ${total_cost:.6f}")
135+
print(f" Total cost: ${_format_float(total_cost)}")
129136
except Exception as e:
130137
# Don't fail session stop if metrics summary fails
131138
logger.warning(f"Failed to print session usage summary: {e}")
132139

140+
# Utility functions
133141

142+
def _format_float(value: float) -> str:
143+
"""Format float up to 6 decimal places, but strip trailing zeros. Always keep at least 2 decimals."""
144+
d = Decimal(value).quantize(Decimal("0.000001"), rounding=ROUND_DOWN) # 6 decimals max
145+
s = format(d.normalize(), "f") # remove exponent notation
146+
integer_part, _, decimal_part = s.partition(".")
147+
# Remove trailing zeros from decimals, then ensure at least 2 digits
148+
decimal_part = (decimal_part.rstrip("0") or "0").ljust(2, "0")
149+
return f"{integer_part}.{decimal_part}"

src/fenic/_backends/local/system_table_client.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
DoubleType,
2727
IntegerType,
2828
StringType,
29+
TimestampType,
2930
)
3031

3132
# Constants for system schema and table names
@@ -630,6 +631,12 @@ def get_metrics_for_session(self, cursor: duckdb.DuckDBPyConnection, session_id:
630631
f"""
631632
SELECT
632633
SUM(total_lm_cost) as total_lm_cost,
634+
SUM(total_lm_uncached_input_tokens) as total_lm_uncached_input_tokens,
635+
SUM(total_lm_cached_input_tokens) as total_lm_cached_input_tokens,
636+
SUM(total_lm_output_tokens) as total_lm_output_tokens,
637+
SUM(total_lm_requests) as total_lm_requests,
638+
SUM(total_rm_input_tokens) as total_rm_input_tokens,
639+
SUM(total_rm_requests) as total_rm_requests,
633640
SUM(total_rm_cost) as total_rm_cost,
634641
COUNT(*) as query_count,
635642
SUM(execution_time_ms) as total_execution_time_ms,
@@ -643,6 +650,12 @@ def get_metrics_for_session(self, cursor: duckdb.DuckDBPyConnection, session_id:
643650
if result is None:
644651
return {
645652
"total_lm_cost": 0.0,
653+
"total_lm_uncached_input_tokens": 0,
654+
"total_lm_cached_input_tokens": 0,
655+
"total_lm_output_tokens": 0,
656+
"total_lm_requests": 0,
657+
"total_rm_input_tokens": 0,
658+
"total_rm_requests": 0,
646659
"total_rm_cost": 0.0,
647660
"query_count": 0,
648661
"total_execution_time_ms": 0.0,
@@ -651,10 +664,16 @@ def get_metrics_for_session(self, cursor: duckdb.DuckDBPyConnection, session_id:
651664

652665
return {
653666
"total_lm_cost": result[0],
654-
"total_rm_cost": result[1],
655-
"query_count": result[2],
656-
"total_execution_time_ms": result[3],
657-
"total_output_rows": result[4],
667+
"total_lm_uncached_input_tokens": result[1],
668+
"total_lm_cached_input_tokens": result[2],
669+
"total_lm_output_tokens": result[3],
670+
"total_lm_requests": result[4],
671+
"total_rm_input_tokens": result[5],
672+
"total_rm_requests": result[6],
673+
"total_rm_cost": result[7],
674+
"query_count": result[8],
675+
"total_execution_time_ms": result[9],
676+
"total_output_rows": result[10],
658677
}
659678

660679
except Exception as e:
@@ -713,8 +732,8 @@ def _initialize_read_only_system_schema_and_tables(self, cursor: duckdb.DuckDBPy
713732
session_id TEXT NOT NULL,
714733
execution_time_ms DOUBLE NOT NULL,
715734
num_output_rows INTEGER NOT NULL,
716-
start_ts TIMESTAMP NOT NULL,
717-
end_ts TIMESTAMP NOT NULL,
735+
start_ts TIMESTAMPTZ NOT NULL,
736+
end_ts TIMESTAMPTZ NOT NULL,
718737
total_lm_cost DOUBLE NOT NULL DEFAULT 0.0,
719738
total_lm_uncached_input_tokens INTEGER NOT NULL DEFAULT 0,
720739
total_lm_cached_input_tokens INTEGER NOT NULL DEFAULT 0,
@@ -733,8 +752,8 @@ def _initialize_read_only_system_schema_and_tables(self, cursor: duckdb.DuckDBPy
733752
ColumnField(name="session_id", data_type=StringType),
734753
ColumnField(name="execution_time_ms", data_type=DoubleType),
735754
ColumnField(name="num_output_rows", data_type=IntegerType),
736-
ColumnField(name="start_ts", data_type=StringType), # Store as ISO timestamp string
737-
ColumnField(name="end_ts", data_type=StringType), # Store as ISO timestamp string
755+
ColumnField(name="start_ts", data_type=TimestampType),
756+
ColumnField(name="end_ts", data_type=TimestampType),
738757
ColumnField(name="total_lm_cost", data_type=DoubleType),
739758
ColumnField(name="total_lm_uncached_input_tokens", data_type=IntegerType),
740759
ColumnField(name="total_lm_cached_input_tokens", data_type=IntegerType),

tests/_backends/local/catalog/test_metrics_table.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import os
2+
import zoneinfo
3+
from datetime import datetime
24

35
import pytest
46

@@ -117,6 +119,15 @@ def test_metrics_table_contains_execution_data(local_session: Session, sample_df
117119
assert latest_metric["execution_id"][0] == execution_id
118120
assert latest_metric["start_ts"][0] is not None
119121
assert latest_metric["end_ts"][0] is not None
122+
123+
# Check that start_ts and end_ts are datetime objects with UTC timezone
124+
start_ts = latest_metric["start_ts"][0]
125+
end_ts = latest_metric["end_ts"][0]
126+
127+
assert isinstance(start_ts, datetime)
128+
assert isinstance(end_ts, datetime)
129+
assert start_ts.tzinfo == zoneinfo.ZoneInfo(key='UTC')
130+
assert end_ts.tzinfo == zoneinfo.ZoneInfo(key='UTC')
120131

121132

122133
def test_multiple_sessions_different_metrics(tmp_path, local_session_config: SessionConfig):

0 commit comments

Comments
 (0)