From 3cdaf30a24c2bff45cd8ad7f0ef841b80ddb2e5d Mon Sep 17 00:00:00 2001 From: Niraj Nepal Date: Fri, 13 Feb 2026 16:32:15 +0100 Subject: [PATCH 1/2] fix: Add Firebase dashboard resource attributes for Python telemetry visibility --- .../plugins/google_cloud/telemetry/config.py | 64 +++++++++++++------ .../google_cloud/telemetry/trace_exporter.py | 4 ++ py/plugins/google-cloud/tests/tracing_test.py | 13 +++- 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/py/plugins/google-cloud/src/genkit/plugins/google_cloud/telemetry/config.py b/py/plugins/google-cloud/src/genkit/plugins/google_cloud/telemetry/config.py index 5b7181b4ca..586853cb80 100644 --- a/py/plugins/google-cloud/src/genkit/plugins/google_cloud/telemetry/config.py +++ b/py/plugins/google-cloud/src/genkit/plugins/google_cloud/telemetry/config.py @@ -32,10 +32,11 @@ from opentelemetry.resourcedetector.gcp_resource_detector import GoogleCloudResourceDetector from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader -from opentelemetry.sdk.resources import SERVICE_INSTANCE_ID, SERVICE_NAME, Resource +from opentelemetry.sdk.resources import SERVICE_INSTANCE_ID, SERVICE_NAME, SERVICE_VERSION, Resource from opentelemetry.sdk.trace.sampling import Sampler from opentelemetry.trace import get_current_span, span as trace_span +from genkit.core import GENKIT_VERSION from genkit.core.environment import is_dev_environment from genkit.core.logging import get_logger from genkit.core.tracing import add_custom_exporter @@ -237,6 +238,9 @@ def _configure_tracing(self) -> None: return try: + # Create Firebase-compatible resource for consistent labeling + resource = self._create_firebase_resource() + exporter_kwargs = self._build_exporter_kwargs() base_exporter = GenkitGCPExporter(**exporter_kwargs) if exporter_kwargs else GenkitGCPExporter() @@ -245,35 +249,57 @@ def _configure_tracing(self) -> None: log_input_and_output=self.log_input_and_output, project_id=self.project_id, error_handler=handle_tracing_error, + resource=resource, # Pass resource for consistent attribution ) add_custom_exporter(trace_exporter, 'gcp_telemetry_server') except Exception as e: handle_tracing_error(e) + def _create_firebase_resource(self) -> Resource: + """Create resource with Firebase-compatible attributes for dashboard recognition. + + This matches the resource configuration patterns used by JS and Go implementations + to ensure the Firebase Genkit monitoring dashboard can properly filter and display + telemetry data. + """ + # Base resource with Firebase-required attributes + resource_attributes = { + SERVICE_NAME: 'genkit', + SERVICE_VERSION: GENKIT_VERSION, + SERVICE_INSTANCE_ID: str(uuid.uuid4()), + 'type': 'global', + 'source': 'py', + 'sourceVersion': GENKIT_VERSION, + } + + base_resource = Resource.create(resource_attributes) + + # Merge with GCP resource detection (matches JS/Go pattern) + # Suppress detector warnings during GCP resource detection + detector_logger = logging.getLogger('opentelemetry.resourcedetector.gcp_resource_detector') + original_level = detector_logger.level + detector_logger.setLevel(logging.ERROR) + + try: + gcp_resource = GoogleCloudResourceDetector(raise_on_error=True).detect() + merged_resource = base_resource.merge(gcp_resource) + logger.debug('Successfully merged Firebase base resource with GCP resource detection') + return merged_resource + except Exception as e: + # For detection failure, log and use the base resource + logger.warning('Google Cloud resource detection failed, using base resource', error=str(e)) + return base_resource + finally: + detector_logger.setLevel(original_level) + def _configure_metrics(self) -> None: if self.disable_metrics: return try: - resource = Resource.create({ - SERVICE_NAME: 'genkit', - SERVICE_INSTANCE_ID: str(uuid.uuid4()), - }) - - # Suppress detector warnings during GCP resource detection - detector_logger = logging.getLogger('opentelemetry.resourcedetector.gcp_resource_detector') - original_level = detector_logger.level - detector_logger.setLevel(logging.ERROR) - - try: - gcp_resource = GoogleCloudResourceDetector(raise_on_error=True).detect() - resource = resource.merge(gcp_resource) - except Exception as e: - # For detection failure log the exception and use the default resource - detector_logger.warning(f'Google Cloud resource detection failed: {e}') - finally: - detector_logger.setLevel(original_level) + # Use Firebase-compatible resource configuration + resource = self._create_firebase_resource() exporter_kwargs = self._build_exporter_kwargs() cloud_monitoring_exporter = CloudMonitoringMetricsExporter(**exporter_kwargs) diff --git a/py/plugins/google-cloud/src/genkit/plugins/google_cloud/telemetry/trace_exporter.py b/py/plugins/google-cloud/src/genkit/plugins/google_cloud/telemetry/trace_exporter.py index 81b5a202bb..7f60496d95 100644 --- a/py/plugins/google-cloud/src/genkit/plugins/google_cloud/telemetry/trace_exporter.py +++ b/py/plugins/google-cloud/src/genkit/plugins/google_cloud/telemetry/trace_exporter.py @@ -26,6 +26,7 @@ from google.api_core import exceptions as core_exceptions, retry as retries from google.cloud.trace_v2 import BatchWriteSpansRequest from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter +from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult @@ -154,6 +155,7 @@ def __init__( log_input_and_output: bool = False, project_id: str | None = None, error_handler: Callable[[Exception], None] | None = None, + resource: 'Resource | None' = None, ) -> None: """Initialize the GCP adjusting trace exporter. @@ -163,7 +165,9 @@ def __init__( Defaults to False (redact for privacy). project_id: Optional GCP project ID for log correlation. error_handler: Optional callback invoked when export errors occur. + resource: Optional OpenTelemetry resource for consistent attribution. """ + self.resource = resource super().__init__( exporter=exporter, log_input_and_output=log_input_and_output, diff --git a/py/plugins/google-cloud/tests/tracing_test.py b/py/plugins/google-cloud/tests/tracing_test.py index 1bc80984c5..32186a0267 100644 --- a/py/plugins/google-cloud/tests/tracing_test.py +++ b/py/plugins/google-cloud/tests/tracing_test.py @@ -187,7 +187,7 @@ def test_add_gcp_telemetry_disable_traces() -> None: def test_add_gcp_telemetry_disable_metrics() -> None: - """Test that disable_metrics=True skips metrics export (JS/Go parity).""" + """Test that disable_metrics=True skips metrics export but still enables traces with resource detection.""" with ( mock.patch.dict(os.environ, {EnvVar.GENKIT_ENV: GenkitEnvironment.PROD}), patch('genkit.plugins.google_cloud.telemetry.config.GenkitGCPExporter'), @@ -199,17 +199,24 @@ def test_add_gcp_telemetry_disable_metrics() -> None: patch('genkit.plugins.google_cloud.telemetry.config.PeriodicExportingMetricReader') as mock_reader, patch('genkit.plugins.google_cloud.telemetry.config.metrics'), ): + # Configure mock detector to return a mock resource + mock_resource = mock.MagicMock() + mock_detector.return_value.detect.return_value = mock_resource + from genkit.plugins.google_cloud.telemetry.tracing import add_gcp_telemetry # Call with disable_metrics=True (JS/Go: disableMetrics) add_gcp_telemetry(disable_metrics=True) - # Verify metrics exporter was NOT created - mock_detector.assert_not_called() + # Verify metrics exporters were NOT created (metrics disabled) mock_metric_exp.assert_not_called() mock_genkit_metric.assert_not_called() mock_reader.assert_not_called() + # Verify resource detection was called for traces (Firebase dashboard needs this) + mock_detector.assert_called_once_with(raise_on_error=True) + mock_detector.return_value.detect.assert_called_once() + def test_add_gcp_telemetry_custom_metric_interval() -> None: """Test that metric_export_interval_ms is passed correctly (JS/Go parity).""" From b67d85ba46e51a8ebbd1204173ea31faf82d6334 Mon Sep 17 00:00:00 2001 From: Niraj Nepal Date: Fri, 13 Feb 2026 17:57:41 +0100 Subject: [PATCH 2/2] fix: Match impl with js --- .../google_cloud/telemetry/gcp_logger.py | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/py/plugins/google-cloud/src/genkit/plugins/google_cloud/telemetry/gcp_logger.py b/py/plugins/google-cloud/src/genkit/plugins/google_cloud/telemetry/gcp_logger.py index f2345b973f..cf3be53038 100644 --- a/py/plugins/google-cloud/src/genkit/plugins/google_cloud/telemetry/gcp_logger.py +++ b/py/plugins/google-cloud/src/genkit/plugins/google_cloud/telemetry/gcp_logger.py @@ -181,12 +181,24 @@ def _write(self, message: str, payload: dict[str, Any], severity: str) -> None: Args: message: Log message for fallback logging. - payload: Structured payload with all metadata. + payload: Structured payload with nested metadata. severity: Cloud Logging severity (INFO, ERROR). """ if self._export and self._cloud_logger: try: - self._cloud_logger.log_struct(payload, severity=severity, labels={'module': 'genkit'}) + from google.cloud.logging_v2.resource import Resource as MonitoredResource + + # Attach resource for proper dashboard filtering + resource = MonitoredResource( + type='global', labels={'project_id': self._project_id} if self._project_id else {} + ) + + self._cloud_logger.log_struct( + payload, + severity=severity, + labels={'module': 'genkit'}, + resource=resource, + ) except Exception as e: logger.error('Failed to write to Cloud Logging', error=str(e), message=message) # Fallback to console @@ -224,8 +236,9 @@ def log_structured(self, message: str, metadata: dict[str, Any] | None = None) - logger.warning(f'[FALLBACK] {message}', **(metadata or {})) return - payload = metadata.copy() if metadata else {} - payload['message'] = message + # Create nested payload: {message, metadata: {...}, trace fields} + metadata_dict = metadata.copy() if metadata else {} + payload = {'message': message, 'metadata': metadata_dict} payload.update(self._get_trace_context()) self._write(message, payload, 'INFO') @@ -236,8 +249,8 @@ def log_structured_error(self, message: str, metadata: dict[str, Any] | None = N message: Log message. metadata: Additional structured metadata. """ - payload = metadata.copy() if metadata else {} - payload['message'] = message + metadata_dict = metadata.copy() if metadata else {} + payload = {'message': message, 'metadata': metadata_dict} payload.update(self._get_trace_context()) self._write(message, payload, 'ERROR')