|
@dataclass |
|
class RetryConfig: |
|
"""Configuration for retrying failed BigQuery write operations. |
|
|
|
Attributes: |
|
max_retries: Maximum number of retry attempts. |
|
initial_delay: Initial delay between retries in seconds. |
|
multiplier: Multiplier for exponential backoff. |
|
max_delay: Maximum delay between retries in seconds. |
|
""" |
|
|
|
max_retries: int = 3 |
|
initial_delay: float = 1.0 |
|
multiplier: float = 2.0 |
|
max_delay: float = 10.0 |
|
|
|
|
|
@dataclass |
|
class BigQueryLoggerConfig: |
|
"""Configuration for the BigQueryAgentAnalyticsPlugin. |
|
|
|
Attributes: |
|
enabled: Whether logging is enabled. |
|
event_allowlist: list of event types to log. If None, all are allowed. |
|
event_denylist: list of event types to ignore. |
|
max_content_length: Max length for text content before truncation. |
|
table_id: BigQuery table ID. |
|
clustering_fields: Fields to cluster the table by. |
|
log_multi_modal_content: Whether to log detailed content parts. |
|
retry_config: Retry configuration for writes. |
|
batch_size: Number of rows per batch. |
|
batch_flush_interval: Max time to wait before flushing a batch. |
|
shutdown_timeout: Max time to wait for shutdown. |
|
queue_max_size: Max size of the in-memory queue. |
|
content_formatter: Optional custom formatter for content. |
|
gcs_bucket_name: GCS bucket for offloading large content. |
|
connection_id: BigQuery connection ID for ObjectRef columns. |
|
log_session_metadata: Whether to log session metadata. |
|
custom_tags: Static custom tags to attach to every event. |
|
auto_schema_upgrade: Whether to auto-add new columns on schema evolution. |
|
create_views: Whether to auto-create per-event-type views. |
|
view_prefix: Prefix for auto-created view names. Default ``"v"`` produces |
|
views like ``v_llm_request``. Set a distinct prefix per table when |
|
multiple plugin instances share one dataset to avoid view-name |
|
collisions. |
|
enable_otel_correlation: When ``True``, capture the ambient OpenTelemetry |
|
span context at row-emission time into ``attributes.otel.{span_id, |
|
trace_id}`` (a best-effort Cloud Trace join key, not a foreign key). |
|
``False`` (the default) emits no ``attributes.otel``. Has no effect when |
|
``attributes`` is projected out via ``payload_column_denylist``. |
|
custom_metadata_allowlist: Keys to capture from ``event.custom_metadata`` |
|
into ``attributes.custom_metadata.*``. Entries are exact keys, or |
|
explicit prefix patterns ending in ``*`` (e.g. ``"a2a:*"``). ``None`` / |
|
empty preserves today's behavior (only the built-in ``a2a:*`` path |
|
runs). Captured values pass the same safety pipeline (truncation, |
|
sensitive-key redaction, circular-reference handling) as all other |
|
logged content. |
|
payload_column_denylist: Payload columns to project OUT of the table at |
|
write time. Only the projectable payload columns ``content`` / |
|
``content_parts`` / ``attributes`` / ``latency_ms`` may be listed; |
|
identity / correlation columns are protected and raise ``ValueError`` if |
|
listed. Applied schema-first (table schema, Arrow schema, row dict, and |
|
views all stay consistent); views that reference a denied column drop |
|
the dependent derived columns. NOTE: denying ``attributes`` also |
|
disables ``attributes.otel`` and ``attributes.custom_metadata``; |
|
combining it with a non-empty ``custom_metadata_allowlist`` is |
|
rejected at construction. |
|
""" |
|
|
|
enabled: bool = True |
|
|
|
# V1 Configuration Parity |
|
event_allowlist: list[str] | None = None |
|
event_denylist: list[str] | None = None |
|
max_content_length: int = 500 * 1024 # Defaults to 500KB per text block |
|
table_id: str = "agent_events" |
|
|
|
# V2 Configuration |
|
clustering_fields: list[str] = field( |
|
default_factory=lambda: ["event_type", "agent", "user_id"] |
|
) |
|
log_multi_modal_content: bool = True |
|
retry_config: RetryConfig = field(default_factory=RetryConfig) |
|
batch_size: int = 1 |
|
batch_flush_interval: float = 1.0 |
|
shutdown_timeout: float = 10.0 |
|
queue_max_size: int = 10000 |
|
content_formatter: Optional[Callable[[Any, str], Any]] = None |
|
# If provided, large content (images, audio, video, large text) will be offloaded to this GCS bucket. |
|
gcs_bucket_name: Optional[str] = None |
|
# If provided, this connection ID will be used as the authorizer for ObjectRef columns. |
|
# Format: "location.connection_id" (e.g. "us.my-connection") |
|
connection_id: Optional[str] = None |
|
|
|
# Toggle for session metadata (e.g. gchat thread-id) |
|
log_session_metadata: bool = True |
|
# Static custom tags (e.g. {"agent_role": "sales"}) |
|
custom_tags: dict[str, Any] = field(default_factory=dict) |
|
# Automatically add new columns to existing tables when the plugin |
|
# schema evolves. Only additive changes are made (columns are never |
|
# dropped or altered). Safe to leave enabled; a version label on the |
|
# table ensures the diff runs at most once per schema version. |
|
auto_schema_upgrade: bool = True |
|
# Automatically create per-event-type BigQuery views that unnest |
|
# JSON columns into typed, queryable columns. |
|
create_views: bool = True |
|
# Prefix for auto-created per-event-type view names. |
|
# Default "v" produces views like ``v_llm_request``. Set a distinct |
|
# prefix per table when multiple plugin instances share one dataset |
|
# to avoid view-name collisions (e.g. ``"v_staging"`` → |
|
# ``v_staging_llm_request``). |
|
view_prefix: str = "v" |
|
|
|
# --- span-level Cloud Trace correlation --- |
|
# When True, capture the ambient OpenTelemetry span context into |
|
# ``attributes.otel.{span_id,trace_id}`` at row-emission time. Off by |
|
# default; no plugin-owned span is created. |
|
enable_otel_correlation: bool = False |
|
|
|
# --- generic custom_metadata capture (allowlist) --- |
|
# Exact keys and/or explicit ``*``-suffixed prefix patterns to capture |
|
# from ``event.custom_metadata`` into ``attributes.custom_metadata.*``. |
|
# None/empty preserves today's behavior (only the built-in ``a2a:*`` path). |
|
custom_metadata_allowlist: list[str] | None = None |
|
|
|
# --- physical column projection (denylist-first) --- |
|
# Payload columns to omit from the table at write time. Only the |
|
# projectable payload columns are accepted; identity/correlation columns |
|
# are protected (see ``_PROJECTABLE_PAYLOAD_COLUMNS``). |
|
payload_column_denylist: list[str] | None = None |
|
|
Summary
A fresh review of the Python BigQuery Agent Analytics (BQAA) plugin at
google/adk-python@9d306f5d322eccbc5022ac72407fa8a994f64cf6found four high-priority correctness/privacy gaps and several operational follow-ups.The most important problems are:
This is related to #5112, but it is intentionally narrower: #5112 covers the framework-wide design for non-persistent secret state. The BQAA output boundary should be made safe independently and should not wait for that design.
P1: privacy, data integrity, and silent-loss fixes
1. Make
content_formatterfailure fail closedWhen a configured formatter raises,
_log_eventlogs a warning but leavesraw_contentunchanged. The original content is then parsed and written to BigQuery:adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 3570 to 3575 in 9d306f5
"Secret message"to be written after formatter failure:adk-python/tests/unittests/plugins/test_bigquery_agent_analytics_plugin.py
Lines 727 to 763 in 9d306f5
For a formatter used as a redaction/privacy boundary, failure must never fall back to the unformatted payload.
Proposed behavior:
formatter_failedcounter and log the exception without the original payload.2. Sanitize the complete attributes tree immediately before serialization
_recursive_smart_truncateredacts known sensitive dictionary keys, but only the producers that call it receive that protection. Several producers add values directly:state_deltais copied directly intoextra_attributes:adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 3767 to 3775 in 9d306f5
extra_attributesand staticcustom_tagsare copied directly into the assembled map:adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 3412 to 3469 in 9d306f5
adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 3601 to 3616 in 9d306f5
_recursive_smart_truncate:adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 422 to 478 in 9d306f5
As a result, values such as
state_delta.access_token,custom_tags.password, and nested label/extra-attribute secrets can reach BigQuery unchanged. The same helper also does not inspect JSON-encoded string blobs such as credential caches, which is already discussed in #5112.Proposed behavior:
json.dumps.is_truncated.temp:and any accepted futuresecret:scope by contract.3. Remove mutable per-call IDs from the shared async content parser
The plugin reuses one
HybridContentParser, mutatingparser.trace_idandparser.span_idimmediately before each awaited parse:adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 3593 to 3598 in 9d306f5
adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 1623 to 1635 in 9d306f5
adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 1672 to 1680 in 9d306f5
adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 1700 to 1719 in 9d306f5
With two concurrent events containing multiple offloaded parts, event A can resume after event B changes the parser IDs. A's next part then uses B's object name. Both events can write the same
trace-b/span-b_p1object, so the later upload overwrites the earlier one and a BigQuery row can point at another event's content.Proposed behavior:
trace_idandspan_idas immutable per-call arguments, or create an immutable parse context.4. Treat table readiness as a startup requirement and retry failures
_ensure_schema_existscatches general get/create failures and returns normally:adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 2765 to 2802 in 9d306f5
_lazy_setupdoes not inspect a readiness result and continues creating the writer:adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 2678 to 2723 in 9d306f5
_ensure_startedthen marks the plugin started:adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 3161 to 3177 in 9d306f5
A transient control-plane error or failed table creation can therefore leave the plugin permanently "started" against a missing/unready table. Subsequent writes fail and analytics are lost until restart/shutdown.
Proposed behavior:
_started=Trueunless the table is usable.Conflicthandling for concurrent creation, then verify/read the created table as needed.P2: operational hardening
enabled=Falseon every entry point.before_run_callbackcalls_ensure_started()before_log_eventcan apply the enabled guard, and__aenter__also starts unconditionally:adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 3143 to 3148 in 9d306f5
adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 4038 to 4053 in 9d306f5
before_run_callbackawaits the entire setup path, so slow BigQuery control-plane calls delay the agent run. Provide explicit startup/preflight or a bounded asynchronous readiness path with observable loss behavior.adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 668 to 798 in 9d306f5
adk-python/src/google/adk/plugins/bigquery_agent_analytics_plugin.py
Lines 2424 to 2456 in 9d306f5
batch_size, flush/shutdown durations, queue bounds,max_retries, retry delays, and multiplier. In particular,max_retries < 0skips the write loop entirely.BatchProcessor. Setup failures and rows skipped before a processor exists currently cannot appear inget_drop_stats(). Preserve counters across loop cleanup/shutdown and include setup, formatter, shutdown-race, and rejected-append reasons.Acceptance tests
content_formatternever writes the original content._started=False, records loss, and succeeds on a later retry without recreating one setup RPC per event.enabled=Falsecauses zero auth/client/table/view/writer/background-task side effects through Runner callbacks and async context-manager use.Verification signal
These green tests do not cover the safety/lifecycle invariants above.
Related issues
secret:session-state design and JSON-blob/arbitrary-key redaction gaps.