Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions py/plugins/firebase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@
This Genkit plugin provides a set of tools and utilities for working with
Firebase.

## Install
## Telemetry

Firestore integrations (no telemetry):
The Firebase plugin provides easy integration with Google Cloud Observability (Cloud Trace and Cloud Monitoring).

```bash
pip install genkit-plugin-firebase
```
To enable telemetry:

Telemetry export to Google Cloud Observability (Cloud Trace + Cloud Monitoring):
```python
from genkit.plugins.firebase import add_firebase_telemetry

```bash
pip install "genkit-plugin-firebase[telemetry]"
# Enable telemetry (defaults to production-only export)
add_firebase_telemetry()
```

### Configuration

`add_firebase_telemetry` supports the following options:

- `project_id`: Firebase project ID (optional, auto-detected).
- `force_dev_export`: Set to `True` to export telemetry in dev environment (defaults to `False`).
- `log_input_and_output`: Set to `True` to log model inputs and outputs (defaults to `False` / redacted).
- `disable_metrics`: Set to `True` to disable metrics export.
- `disable_traces`: Set to `True` to disable trace export.
1 change: 1 addition & 0 deletions py/plugins/firebase/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ classifiers = [
]
dependencies = [
"genkit",
"genkit-plugin-google-cloud",
"google-cloud-firestore",
"strenum>=0.4.15; python_version < '3.11'",
]
Expand Down
65 changes: 60 additions & 5 deletions py/plugins/firebase/src/genkit/plugins/firebase/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@
- Genkit documentation: https://genkit.dev/
"""

from typing import Any

from opentelemetry.sdk.trace.sampling import Sampler

from .constant import FirebaseTelemetryConfig
from .firestore import define_firestore_vector_store


Expand All @@ -187,26 +192,76 @@ def package_name() -> str:
return 'genkit.plugins.firebase'


def add_firebase_telemetry() -> None:
def add_firebase_telemetry(
config: FirebaseTelemetryConfig | None = None,
*,
project_id: str | None = None,
credentials: dict[str, Any] | None = None,
sampler: Sampler | None = None,
log_input_and_output: bool = False,
force_dev_export: bool = False,
disable_metrics: bool = False,
disable_traces: bool = False,
metric_export_interval_ms: int | None = None,
metric_export_timeout_ms: int | None = None,
) -> None:
"""Add Firebase telemetry export to Google Cloud Observability.

Exports traces to Cloud Trace and metrics to Cloud Monitoring.
In development (GENKIT_ENV=dev), telemetry is disabled by default.
Exports traces to Cloud Trace, metrics to Cloud Monitoring, and logs to
Cloud Logging. In development (GENKIT_ENV=dev), telemetry is disabled by
default unless force_dev_export is True.

Args:
config: FirebaseTelemetryConfig object. If provided, kwargs are ignored.
project_id: Firebase project ID. Auto-detected from environment if None.
credentials: Service account credentials dictionary.
sampler: OpenTelemetry trace sampler.
log_input_and_output: If True, logs feature inputs/outputs. WARNING: May log PII.
force_dev_export: If True, exports in dev mode.
disable_metrics: If True, disables metrics export.
disable_traces: If True, disables trace export.
metric_export_interval_ms: Metrics export interval in ms. Minimum: 1000ms.
metric_export_timeout_ms: Metrics export timeout in ms.

Example::

# Using kwargs
add_firebase_telemetry(project_id='my-project', log_input_and_output=True)

# Using config object
config = FirebaseTelemetryConfig(project_id='my-project')
add_firebase_telemetry(config)
"""
try:
# Imported lazily so Firestore-only users don't need telemetry deps.
from genkit.plugins.google_cloud.telemetry.tracing import add_gcp_telemetry
from .telemetry import add_firebase_telemetry as _add_firebase_telemetry
except ImportError as e:
raise ImportError(
'Firebase telemetry requires the Google Cloud telemetry exporter. '
'Install it with: pip install "genkit-plugin-firebase[telemetry]"'
) from e

add_gcp_telemetry(force_export=False)
if config is not None:
_add_firebase_telemetry(config)
else:
_add_firebase_telemetry(
FirebaseTelemetryConfig(
project_id=project_id,
credentials=credentials,
sampler=sampler,
log_input_and_output=log_input_and_output,
force_dev_export=force_dev_export,
disable_metrics=disable_metrics,
disable_traces=disable_traces,
metric_export_interval_ms=metric_export_interval_ms,
metric_export_timeout_ms=metric_export_timeout_ms,
)
)


__all__ = [
'add_firebase_telemetry',
'define_firestore_vector_store',
'FirebaseTelemetryConfig',
'package_name',
]
33 changes: 31 additions & 2 deletions py/plugins/firebase/src/genkit/plugins/firebase/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,40 @@
#
# SPDX-License-Identifier: Apache-2.0

"""Firebase constants."""
"""Firebase constants and configuration models."""

from collections.abc import Callable
from typing import Any
from typing import Annotated, Any

from google.cloud.firestore_v1 import DocumentSnapshot
from opentelemetry.sdk.trace.sampling import Sampler
from pydantic import BaseModel, Field

MetadataTransformFn = Callable[[DocumentSnapshot], dict[str, Any]]


class FirebaseTelemetryConfig(BaseModel):
"""Configuration for Firebase telemetry export to Google Cloud Observability.

Args:
project_id: Firebase project ID. Auto-detected from environment if None.
credentials: Service account credentials dictionary.
sampler: OpenTelemetry trace sampler.
log_input_and_output: If True, logs feature inputs/outputs. WARNING: May log PII.
force_dev_export: If True, exports telemetry in dev mode (GENKIT_ENV=dev).
disable_metrics: If True, disables metrics export.
disable_traces: If True, disables trace export.
metric_export_interval_ms: Metrics export interval in ms. Minimum: 1000ms.
metric_export_timeout_ms: Metrics export timeout in ms.
"""

project_id: str | None = None
credentials: dict[str, Any] | None = None
sampler: Sampler | None = None
log_input_and_output: bool = False
force_dev_export: bool = False
disable_metrics: bool = False
disable_traces: bool = False
metric_export_interval_ms: Annotated[int, Field(ge=1000)] | None = None
metric_export_timeout_ms: int | None = None
model_config = {'arbitrary_types_allowed': True}
55 changes: 55 additions & 0 deletions py/plugins/firebase/src/genkit/plugins/firebase/telemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0

"""Firebase telemetry integration."""

from genkit.core.logging import get_logger
from genkit.plugins.google_cloud.telemetry.config import GcpTelemetry

from .constant import FirebaseTelemetryConfig

logger = get_logger(__name__)


def add_firebase_telemetry(config: FirebaseTelemetryConfig) -> None:
"""Add Firebase telemetry export to Google Cloud Observability.

Exports traces to Cloud Trace, metrics to Cloud Monitoring, and logs to
Cloud Logging. In development (GENKIT_ENV=dev), telemetry is disabled by
default unless force_dev_export is set to True.

Args:
config: FirebaseTelemetryConfig object with telemetry configuration.
"""
manager = GcpTelemetry(
project_id=config.project_id,
credentials=config.credentials,
sampler=config.sampler,
log_input_and_output=config.log_input_and_output,
force_dev_export=config.force_dev_export,
disable_metrics=config.disable_metrics,
disable_traces=config.disable_traces,
metric_export_interval_ms=config.metric_export_interval_ms,
metric_export_timeout_ms=config.metric_export_timeout_ms,
)

if not manager.project_id:
logger.warning(
'Firebase project ID not found. Set FIREBASE_PROJECT_ID, GOOGLE_CLOUD_PROJECT, '
'or GCLOUD_PROJECT environment variable, or pass project_id parameter.'
)

manager.initialize()
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,35 @@ def _create_model_span(
return mock_span


@patch('genkit.plugins.google_cloud.telemetry.tracing.add_gcp_telemetry')
def test_firebase_telemetry_delegates_to_gcp(mock_add_gcp_telemetry: MagicMock) -> None:
"""Test that Firebase telemetry delegates to GCP telemetry."""
@patch('genkit.plugins.firebase.telemetry.GcpTelemetry')
def test_firebase_telemetry_initializes_gcp_telemetry(mock_gcp_telemetry_cls: MagicMock) -> None:
"""Test that Firebase telemetry initializes GcpTelemetry with correct defaults."""
mock_manager = MagicMock()
mock_gcp_telemetry_cls.return_value = mock_manager

add_firebase_telemetry()
mock_add_gcp_telemetry.assert_called_once_with(force_export=False)

mock_gcp_telemetry_cls.assert_called_once()
kwargs = mock_gcp_telemetry_cls.call_args.kwargs
assert kwargs['force_dev_export'] is False
mock_manager.initialize.assert_called_once()


def test_firebase_telemetry_passes_configuration() -> None:
"""Test that configuration options are passed to GcpTelemetry."""
with patch('genkit.plugins.firebase.telemetry.GcpTelemetry') as mock_gcp_telemetry_cls:
add_firebase_telemetry(
project_id='test-project',
log_input_and_output=True,
force_dev_export=True,
disable_metrics=True,
)

kwargs = mock_gcp_telemetry_cls.call_args.kwargs
assert kwargs['project_id'] == 'test-project'
assert kwargs['log_input_and_output'] is True
assert kwargs['force_dev_export'] is True
assert kwargs['disable_metrics'] is True


@patch('genkit.plugins.google_cloud.telemetry.metrics._output_tokens')
Expand Down
53 changes: 49 additions & 4 deletions py/plugins/firebase/tests/firebase_plugin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import pytest

from genkit.plugins.firebase import (
FirebaseTelemetryConfig,
add_firebase_telemetry,
define_firestore_vector_store,
package_name,
Expand All @@ -33,11 +34,43 @@ def test_package_name() -> None:
assert package_name() == 'genkit.plugins.firebase'


@patch('genkit.plugins.google_cloud.telemetry.tracing.add_gcp_telemetry')
def test_add_firebase_telemetry_calls_gcp_telemetry(mock_add_gcp: MagicMock) -> None:
@patch('genkit.plugins.firebase.telemetry.GcpTelemetry')
def test_add_firebase_telemetry_calls_gcp_telemetry(mock_gcp_telemetry_cls: MagicMock) -> None:
"""Test add_firebase_telemetry delegates to GCP telemetry."""
mock_manager = MagicMock()
mock_gcp_telemetry_cls.return_value = mock_manager

add_firebase_telemetry()
mock_add_gcp.assert_called_once_with(force_export=False)

mock_gcp_telemetry_cls.assert_called_once()
mock_manager.initialize.assert_called_once()


@patch('genkit.plugins.firebase.telemetry.GcpTelemetry')
def test_add_firebase_telemetry_with_config(mock_gcp_telemetry_cls: MagicMock) -> None:
"""Test add_firebase_telemetry accepts config object."""
mock_manager = MagicMock()
mock_gcp_telemetry_cls.return_value = mock_manager

config = FirebaseTelemetryConfig(
project_id='test-project',
log_input_and_output=True,
force_dev_export=True,
)
add_firebase_telemetry(config)

mock_gcp_telemetry_cls.assert_called_once_with(
project_id='test-project',
credentials=None,
sampler=None,
log_input_and_output=True,
force_dev_export=True,
disable_metrics=False,
disable_traces=False,
metric_export_interval_ms=None,
metric_export_timeout_ms=None,
)
mock_manager.initialize.assert_called_once()


def test_define_firestore_vector_store_exported() -> None:
Expand All @@ -49,6 +82,18 @@ def test_define_firestore_vector_store_exported() -> None:
def test_add_firebase_telemetry_raises_on_missing_deps() -> None:
"""Test that an informative ImportError is raised if telemetry deps are missing."""
# Temporarily remove the module from sys.modules to simulate it not being installed.
with patch.dict(sys.modules, {'genkit.plugins.google_cloud.telemetry.tracing': None}):
with patch.dict(sys.modules, {'genkit.plugins.firebase.telemetry': None}):
with pytest.raises(ImportError, match='Firebase telemetry requires the Google Cloud telemetry exporter'):
add_firebase_telemetry()


def test_firebase_telemetry_config_validation() -> None:
"""Test Pydantic validation on FirebaseTelemetryConfig."""
# Valid config
config = FirebaseTelemetryConfig(project_id='test-project')
assert config.project_id == 'test-project'
assert config.log_input_and_output is False

# Invalid metric interval (< 1000ms) should raise ValidationError
with pytest.raises(ValueError):
FirebaseTelemetryConfig(metric_export_interval_ms=500)
1 change: 1 addition & 0 deletions py/plugins/google-cloud/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ classifiers = [
]
dependencies = [
"genkit",
"google-cloud-logging>=3.10.0",
"opentelemetry-exporter-gcp-trace>=1.9.0",
"opentelemetry-exporter-gcp-monitoring>=1.9.0",
"strenum>=0.4.15; python_version < '3.11'",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import structlog
from opentelemetry.sdk.trace import ReadableSpan

from .gcp_logger import gcp_logger
from .utils import (
create_common_log_attributes,
extract_outer_feature_name_from_path,
Expand Down Expand Up @@ -103,7 +104,7 @@ def _write_log(
session_id: str | None,
thread_name: str | None,
) -> None:
"""Write a structured log entry."""
"""Write a structured log entry to Cloud Logging."""
path = truncate_path(to_display_path(qualified_path))
metadata = {
**create_common_log_attributes(span, project_id),
Expand All @@ -117,7 +118,7 @@ def _write_log(
if thread_name:
metadata['threadName'] = thread_name

logger.info(f'{tag}[{path}, {feature_name}]', **metadata)
gcp_logger.log_structured(f'{tag}[{path}, {feature_name}]', metadata)


# Singleton instance
Expand Down
Loading
Loading