Skip to content

Commit 37973da

Browse files
haiyuan-eng-googlecopybara-github
authored andcommitted
feat: Add configurable view_prefix to BigQueryLoggerConfig
Adds a configurable `view_prefix` field to `BigQueryLoggerConfig` (default `"v"`) so that multiple plugin instances sharing a dataset can use distinct prefixes to avoid overwriting each other's auto-created analytics views - Validates that `view_prefix` is non-empty at init time - Wires `view_prefix` into `_create_analytics_views` in place of the hardcoded `"v_"` prefix Co-authored-by: Haiyuan Cao <haiyuan@google.com> PiperOrigin-RevId: 894331973
1 parent 435f7c7 commit 37973da

File tree

2 files changed

+96
-4
lines changed

2 files changed

+96
-4
lines changed

src/google/adk/plugins/bigquery_agent_analytics_plugin.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,16 @@ class BigQueryLoggerConfig:
499499
shutdown_timeout: Max time to wait for shutdown.
500500
queue_max_size: Max size of the in-memory queue.
501501
content_formatter: Optional custom formatter for content.
502+
gcs_bucket_name: GCS bucket for offloading large content.
503+
connection_id: BigQuery connection ID for ObjectRef columns.
504+
log_session_metadata: Whether to log session metadata.
505+
custom_tags: Static custom tags to attach to every event.
506+
auto_schema_upgrade: Whether to auto-add new columns on schema evolution.
507+
create_views: Whether to auto-create per-event-type views.
508+
view_prefix: Prefix for auto-created view names. Default ``"v"`` produces
509+
views like ``v_llm_request``. Set a distinct prefix per table when
510+
multiple plugin instances share one dataset to avoid view-name
511+
collisions.
502512
"""
503513

504514
enabled: bool = True
@@ -538,6 +548,12 @@ class BigQueryLoggerConfig:
538548
# Automatically create per-event-type BigQuery views that unnest
539549
# JSON columns into typed, queryable columns.
540550
create_views: bool = True
551+
# Prefix for auto-created per-event-type view names.
552+
# Default "v" produces views like ``v_llm_request``. Set a distinct
553+
# prefix per table when multiple plugin instances share one dataset
554+
# to avoid view-name collisions (e.g. ``"v_staging"`` →
555+
# ``v_staging_llm_request``).
556+
view_prefix: str = "v"
541557

542558

543559
# ==============================================================================
@@ -1878,6 +1894,9 @@ def __init__(
18781894
else:
18791895
logger.warning(f"Unknown configuration parameter: {key}")
18801896

1897+
if not self.config.view_prefix:
1898+
raise ValueError("view_prefix must be a non-empty string.")
1899+
18811900
self.table_id = table_id or self.config.table_id
18821901
self.location = location
18831902

@@ -2314,7 +2333,7 @@ def _create_analytics_views(self) -> None:
23142333
Errors are logged but never raised.
23152334
"""
23162335
for event_type, extra_cols in _EVENT_VIEW_DEFS.items():
2317-
view_name = "v_" + event_type.lower()
2336+
view_name = self.config.view_prefix + "_" + event_type.lower()
23182337
columns = ",\n ".join(list(_VIEW_COMMON_COLUMNS) + extra_cols)
23192338
sql = _VIEW_SQL_TEMPLATE.format(
23202339
project=self.project_id,

tests/unittests/plugins/test_bigquery_agent_analytics_plugin.py

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5087,18 +5087,19 @@ def test_reset_logs_fork_warning(self):
50875087
class TestAnalyticsViews:
50885088
"""Tests for auto-created per-event-type BigQuery views."""
50895089

5090-
def _make_plugin(self, create_views=True):
5090+
def _make_plugin(self, create_views=True, view_prefix="v", table_id=TABLE_ID):
50915091
config = bigquery_agent_analytics_plugin.BigQueryLoggerConfig(
50925092
create_views=create_views,
5093+
view_prefix=view_prefix,
50935094
)
50945095
plugin = bigquery_agent_analytics_plugin.BigQueryAgentAnalyticsPlugin(
50955096
project_id=PROJECT_ID,
50965097
dataset_id=DATASET_ID,
5097-
table_id=TABLE_ID,
5098+
table_id=table_id,
50985099
config=config,
50995100
)
51005101
plugin.client = mock.MagicMock()
5101-
plugin.full_table_id = f"{PROJECT_ID}.{DATASET_ID}.{TABLE_ID}"
5102+
plugin.full_table_id = f"{PROJECT_ID}.{DATASET_ID}.{table_id}"
51025103
plugin._schema = bigquery_agent_analytics_plugin._get_events_schema()
51035104
return plugin
51045105

@@ -5184,6 +5185,7 @@ def test_config_create_views_default_true(self):
51845185
"""Config create_views defaults to True."""
51855186
config = bigquery_agent_analytics_plugin.BigQueryLoggerConfig()
51865187
assert config.create_views is True
5188+
assert config.view_prefix == "v"
51875189

51885190
@pytest.mark.asyncio
51895191
async def test_create_analytics_views_ensures_started(
@@ -5242,6 +5244,77 @@ async def test_create_analytics_views_raises_on_startup_failure(
52425244
assert exc_info.value.__cause__ is not None
52435245
assert "client boom" in str(exc_info.value.__cause__)
52445246

5247+
def test_custom_view_prefix(self):
5248+
"""Custom view_prefix namespaces view names."""
5249+
plugin = self._make_plugin(view_prefix="v_staging")
5250+
plugin.client.get_table.side_effect = cloud_exceptions.NotFound("not found")
5251+
mock_query_job = mock.MagicMock()
5252+
plugin.client.query.return_value = mock_query_job
5253+
5254+
plugin._ensure_schema_exists()
5255+
5256+
calls = plugin.client.query.call_args_list
5257+
all_sql = " ".join(c[0][0] for c in calls)
5258+
# All views should use the custom prefix
5259+
for event_type in bigquery_agent_analytics_plugin._EVENT_VIEW_DEFS:
5260+
expected_name = "v_staging_" + event_type.lower()
5261+
assert expected_name in all_sql, f"View {expected_name} not found in SQL"
5262+
# Default prefix should NOT appear
5263+
assert ".v_llm_request" not in all_sql
5264+
5265+
def test_default_view_prefix_preserves_names(self):
5266+
"""Default view_prefix='v' produces the same names as before."""
5267+
plugin = self._make_plugin() # default view_prefix="v"
5268+
plugin.client.get_table.side_effect = cloud_exceptions.NotFound("not found")
5269+
mock_query_job = mock.MagicMock()
5270+
plugin.client.query.return_value = mock_query_job
5271+
5272+
plugin._ensure_schema_exists()
5273+
5274+
calls = plugin.client.query.call_args_list
5275+
all_sql = " ".join(c[0][0] for c in calls)
5276+
for event_type in bigquery_agent_analytics_plugin._EVENT_VIEW_DEFS:
5277+
view_name = "v_" + event_type.lower()
5278+
assert view_name in all_sql
5279+
5280+
def test_distinct_tables_and_prefixes_no_collision(self):
5281+
"""Two plugins targeting different tables produce disjoint views."""
5282+
plugin_a = self._make_plugin(
5283+
table_id="agent_events_prod", view_prefix="v_prod"
5284+
)
5285+
plugin_b = self._make_plugin(
5286+
table_id="agent_events_staging", view_prefix="v_staging"
5287+
)
5288+
5289+
for plugin in (plugin_a, plugin_b):
5290+
plugin.client.get_table.side_effect = cloud_exceptions.NotFound(
5291+
"not found"
5292+
)
5293+
mock_query_job = mock.MagicMock()
5294+
plugin.client.query.return_value = mock_query_job
5295+
plugin._ensure_schema_exists()
5296+
5297+
sql_a = " ".join(c[0][0] for c in plugin_a.client.query.call_args_list)
5298+
sql_b = " ".join(c[0][0] for c in plugin_b.client.query.call_args_list)
5299+
5300+
# View names use their own prefix
5301+
assert "v_prod_llm_request" in sql_a
5302+
assert "v_staging_llm_request" in sql_b
5303+
# No cross-contamination
5304+
assert "v_staging_" not in sql_a
5305+
assert "v_prod_" not in sql_b
5306+
5307+
# FROM clauses point at the correct table
5308+
assert "agent_events_prod" in sql_a
5309+
assert "agent_events_staging" not in sql_a
5310+
assert "agent_events_staging" in sql_b
5311+
assert "agent_events_prod" not in sql_b
5312+
5313+
def test_empty_view_prefix_raises(self):
5314+
"""Empty view_prefix is rejected at init."""
5315+
with pytest.raises(ValueError, match="view_prefix"):
5316+
self._make_plugin(view_prefix="")
5317+
52455318

52465319
# ==============================================================================
52475320
# Trace-ID Continuity Tests (Issue #4645)

0 commit comments

Comments
 (0)