genkit-plugin-google-cloud 0.3.2__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- genkit/plugins/google_cloud/__init__.py +127 -2
- genkit/plugins/google_cloud/telemetry/__init__.py +74 -0
- genkit/plugins/google_cloud/telemetry/action.py +124 -0
- genkit/plugins/google_cloud/telemetry/engagement.py +170 -0
- genkit/plugins/google_cloud/telemetry/feature.py +186 -0
- genkit/plugins/google_cloud/telemetry/generate.py +605 -0
- genkit/plugins/google_cloud/telemetry/metrics.py +246 -0
- genkit/plugins/google_cloud/telemetry/path.py +157 -0
- genkit/plugins/google_cloud/telemetry/tracing.py +880 -29
- genkit/plugins/google_cloud/telemetry/utils.py +217 -0
- {genkit_plugin_google_cloud-0.3.2.dist-info → genkit_plugin_google_cloud-0.5.0.dist-info}/METADATA +10 -2
- genkit_plugin_google_cloud-0.5.0.dist-info/RECORD +15 -0
- {genkit_plugin_google_cloud-0.3.2.dist-info → genkit_plugin_google_cloud-0.5.0.dist-info}/WHEEL +1 -1
- genkit_plugin_google_cloud-0.3.2.dist-info/RECORD +0 -9
- /genkit/{py.typed → plugins/google_cloud/py.typed} +0 -0
- {genkit_plugin_google_cloud-0.3.2.dist-info → genkit_plugin_google_cloud-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Copyright 2025 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
|
|
17
|
+
"""AI monitoring metrics for Genkit.
|
|
18
|
+
|
|
19
|
+
This module provides lazy-initialized OpenTelemetry metrics for AI operations.
|
|
20
|
+
Metrics are exported to Google Cloud Monitoring with the workload.googleapis.com
|
|
21
|
+
prefix by default.
|
|
22
|
+
|
|
23
|
+
Metrics Defined:
|
|
24
|
+
Input metrics:
|
|
25
|
+
- genkit/ai/generate/input/tokens
|
|
26
|
+
- genkit/ai/generate/input/characters
|
|
27
|
+
- genkit/ai/generate/input/images
|
|
28
|
+
- genkit/ai/generate/input/videos
|
|
29
|
+
- genkit/ai/generate/input/audio
|
|
30
|
+
|
|
31
|
+
Output metrics:
|
|
32
|
+
- genkit/ai/generate/output/tokens
|
|
33
|
+
- genkit/ai/generate/output/characters
|
|
34
|
+
- genkit/ai/generate/output/images
|
|
35
|
+
- genkit/ai/generate/output/videos
|
|
36
|
+
- genkit/ai/generate/output/audio
|
|
37
|
+
|
|
38
|
+
Thinking metrics:
|
|
39
|
+
- genkit/ai/generate/thinking/tokens
|
|
40
|
+
|
|
41
|
+
See Also:
|
|
42
|
+
- Cloud Monitoring Custom Metrics: https://cloud.google.com/monitoring/custom-metrics
|
|
43
|
+
- Workload Metrics: https://cloud.google.com/monitoring/api/metrics_other
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
import contextlib
|
|
47
|
+
import json
|
|
48
|
+
import re
|
|
49
|
+
|
|
50
|
+
import structlog
|
|
51
|
+
from opentelemetry import metrics
|
|
52
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
53
|
+
|
|
54
|
+
logger = structlog.get_logger(__name__)
|
|
55
|
+
|
|
56
|
+
meter = metrics.get_meter('genkit')
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _metric(name: str, desc: str, unit: str = '1') -> tuple[str, str, str]:
|
|
60
|
+
"""Create metric name with genkit/ai/ prefix.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
name: Metric name
|
|
64
|
+
desc: Metric description
|
|
65
|
+
unit: Metric unit (default: '1')
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Tuple of (prefixed_name, description, unit)
|
|
69
|
+
"""
|
|
70
|
+
return f'genkit/ai/{name}', desc, unit
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Metric caches for lazy initialization
|
|
74
|
+
_counter_cache: dict[str, metrics.Counter] = {}
|
|
75
|
+
_histogram_cache: dict[str, metrics.Histogram] = {}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_counter(name: str, desc: str, unit: str = '1') -> metrics.Counter:
|
|
79
|
+
"""Get or create counter metric with lazy initialization.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
name: Metric name
|
|
83
|
+
desc: Metric description
|
|
84
|
+
unit: Metric unit (default: '1')
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
OpenTelemetry Counter metric
|
|
88
|
+
"""
|
|
89
|
+
if name not in _counter_cache:
|
|
90
|
+
_counter_cache[name] = meter.create_counter(name, description=desc, unit=unit)
|
|
91
|
+
return _counter_cache[name]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _get_histogram(name: str, desc: str, unit: str = '1') -> metrics.Histogram:
|
|
95
|
+
"""Get or create histogram metric with lazy initialization.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
name: Metric name
|
|
99
|
+
desc: Metric description
|
|
100
|
+
unit: Metric unit (default: '1')
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
OpenTelemetry Histogram metric
|
|
104
|
+
"""
|
|
105
|
+
if name not in _histogram_cache:
|
|
106
|
+
_histogram_cache[name] = meter.create_histogram(name, description=desc, unit=unit)
|
|
107
|
+
return _histogram_cache[name]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Metric definitions
|
|
111
|
+
def _requests() -> metrics.Counter:
|
|
112
|
+
return _get_counter(*_metric('generate/requests', 'Generate requests'))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _failures() -> metrics.Counter:
|
|
116
|
+
return _get_counter(*_metric('generate/failures', 'Generate failures'))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _latency() -> metrics.Histogram:
|
|
120
|
+
return _get_histogram(*_metric('generate/latency', 'Generate latency', 'ms'))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _input_tokens() -> metrics.Counter:
|
|
124
|
+
return _get_counter(*_metric('generate/input/tokens', 'Input tokens'))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _output_tokens() -> metrics.Counter:
|
|
128
|
+
return _get_counter(*_metric('generate/output/tokens', 'Output tokens'))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _input_characters() -> metrics.Counter:
|
|
132
|
+
return _get_counter(*_metric('generate/input/characters', 'Input characters'))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _output_characters() -> metrics.Counter:
|
|
136
|
+
return _get_counter(*_metric('generate/output/characters', 'Output characters'))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _input_images() -> metrics.Counter:
|
|
140
|
+
return _get_counter(*_metric('generate/input/images', 'Input images'))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _output_images() -> metrics.Counter:
|
|
144
|
+
return _get_counter(*_metric('generate/output/images', 'Output images'))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _input_videos() -> metrics.Counter:
|
|
148
|
+
return _get_counter(*_metric('generate/input/videos', 'Input videos'))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _output_videos() -> metrics.Counter:
|
|
152
|
+
return _get_counter(*_metric('generate/output/videos', 'Output videos'))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _input_audio() -> metrics.Counter:
|
|
156
|
+
return _get_counter(*_metric('generate/input/audio', 'Input audio'))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _output_audio() -> metrics.Counter:
|
|
160
|
+
return _get_counter(*_metric('generate/output/audio', 'Output audio'))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def record_generate_metrics(span: ReadableSpan) -> None:
|
|
164
|
+
"""Record AI monitoring metrics from a model action span.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
span: OpenTelemetry span containing model execution data
|
|
168
|
+
"""
|
|
169
|
+
attrs = span.attributes
|
|
170
|
+
if not attrs:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# Check if this is a model action
|
|
174
|
+
if attrs.get('genkit:type') != 'action' or attrs.get('genkit:metadata:subtype') != 'model':
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
# Extract dimensions
|
|
178
|
+
model = str(attrs.get('genkit:name', '<unknown>'))[:1000]
|
|
179
|
+
path = str(attrs.get('genkit:path', ''))[:1000]
|
|
180
|
+
source = _extract_feature_name(path)
|
|
181
|
+
is_error = not span.status.is_ok
|
|
182
|
+
error = 'error' if is_error else 'none'
|
|
183
|
+
|
|
184
|
+
dimensions = {'model': model, 'source': source, 'error': error}
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
_requests().add(1, dimensions)
|
|
188
|
+
if is_error:
|
|
189
|
+
_failures().add(1, dimensions)
|
|
190
|
+
|
|
191
|
+
# Latency
|
|
192
|
+
latency_ms = None
|
|
193
|
+
if span.end_time and span.start_time:
|
|
194
|
+
latency_ms = (span.end_time - span.start_time) / 1_000_000
|
|
195
|
+
_latency().record(latency_ms, dimensions)
|
|
196
|
+
|
|
197
|
+
usage = {}
|
|
198
|
+
output_json = attrs.get('genkit:output')
|
|
199
|
+
if output_json and isinstance(output_json, str):
|
|
200
|
+
try:
|
|
201
|
+
output_data = json.loads(output_json)
|
|
202
|
+
usage = output_data.get('usage', {})
|
|
203
|
+
except (json.JSONDecodeError, AttributeError):
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
usage_metrics = {
|
|
207
|
+
'inputTokens': _input_tokens,
|
|
208
|
+
'outputTokens': _output_tokens,
|
|
209
|
+
'inputCharacters': _input_characters,
|
|
210
|
+
'outputCharacters': _output_characters,
|
|
211
|
+
'inputImages': _input_images,
|
|
212
|
+
'outputImages': _output_images,
|
|
213
|
+
'inputVideos': _input_videos,
|
|
214
|
+
'outputVideos': _output_videos,
|
|
215
|
+
'inputAudio': _input_audio,
|
|
216
|
+
'outputAudio': _output_audio,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for key, metric_fn in usage_metrics.items():
|
|
220
|
+
value = usage.get(key)
|
|
221
|
+
if value is not None:
|
|
222
|
+
with contextlib.suppress(ValueError, TypeError):
|
|
223
|
+
metric_fn().add(int(value), dimensions)
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.warning('Error recording metrics', error=str(e))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _extract_feature_name(path: str) -> str:
|
|
230
|
+
"""Extract feature name from Genkit action path.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
path: Genkit action path in format '/{name,t:type}' or '/{outer,t:flow}/{inner,t:flow}'
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Extracted feature name or '<unknown>' if path cannot be parsed
|
|
237
|
+
"""
|
|
238
|
+
if not path:
|
|
239
|
+
return '<unknown>'
|
|
240
|
+
|
|
241
|
+
parts = path.split('/')
|
|
242
|
+
if len(parts) < 2:
|
|
243
|
+
return '<unknown>'
|
|
244
|
+
|
|
245
|
+
match = re.match(r'\{([^,}]+)', parts[1])
|
|
246
|
+
return match.group(1) if match else '<unknown>'
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Copyright 2025 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
|
|
17
|
+
"""Path telemetry for GCP.
|
|
18
|
+
|
|
19
|
+
This module tracks path-level failure metrics and logs errors,
|
|
20
|
+
matching the JavaScript implementation.
|
|
21
|
+
|
|
22
|
+
Metrics Recorded:
|
|
23
|
+
- genkit/feature/path/requests: Counter for unique flow paths
|
|
24
|
+
- genkit/feature/path/latency: Histogram for path latency (ms)
|
|
25
|
+
|
|
26
|
+
Cross-Language Parity:
|
|
27
|
+
- JavaScript: js/plugins/google-cloud/src/telemetry/paths.ts
|
|
28
|
+
- Go: go/plugins/googlecloud/paths.go
|
|
29
|
+
|
|
30
|
+
See Also:
|
|
31
|
+
- Cloud Monitoring Custom Metrics: https://cloud.google.com/monitoring/custom-metrics
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import structlog
|
|
37
|
+
from opentelemetry import metrics
|
|
38
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
39
|
+
|
|
40
|
+
from genkit.core import GENKIT_VERSION
|
|
41
|
+
|
|
42
|
+
from .utils import (
|
|
43
|
+
create_common_log_attributes,
|
|
44
|
+
extract_error_message,
|
|
45
|
+
extract_error_name,
|
|
46
|
+
extract_error_stack,
|
|
47
|
+
extract_outer_feature_name_from_path,
|
|
48
|
+
to_display_path,
|
|
49
|
+
truncate_path,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
logger = structlog.get_logger(__name__)
|
|
53
|
+
|
|
54
|
+
# Lazy-initialized metrics
|
|
55
|
+
_path_counter: metrics.Counter | None = None
|
|
56
|
+
_path_latency: metrics.Histogram | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_path_counter() -> metrics.Counter:
|
|
60
|
+
"""Get or create the path requests counter."""
|
|
61
|
+
global _path_counter
|
|
62
|
+
if _path_counter is None:
|
|
63
|
+
meter = metrics.get_meter('genkit')
|
|
64
|
+
_path_counter = meter.create_counter(
|
|
65
|
+
'genkit/feature/path/requests',
|
|
66
|
+
description='Tracks unique flow paths per flow.',
|
|
67
|
+
unit='1',
|
|
68
|
+
)
|
|
69
|
+
return _path_counter
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _get_path_latency() -> metrics.Histogram:
|
|
73
|
+
"""Get or create the path latency histogram."""
|
|
74
|
+
global _path_latency
|
|
75
|
+
if _path_latency is None:
|
|
76
|
+
meter = metrics.get_meter('genkit')
|
|
77
|
+
_path_latency = meter.create_histogram(
|
|
78
|
+
'genkit/feature/path/latency',
|
|
79
|
+
description='Latencies per flow path.',
|
|
80
|
+
unit='ms',
|
|
81
|
+
)
|
|
82
|
+
return _path_latency
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class PathsTelemetry:
|
|
86
|
+
"""Telemetry handler for Genkit paths (error tracking)."""
|
|
87
|
+
|
|
88
|
+
def tick(
|
|
89
|
+
self,
|
|
90
|
+
span: ReadableSpan,
|
|
91
|
+
log_input_and_output: bool,
|
|
92
|
+
project_id: str | None = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Record telemetry for a path span.
|
|
95
|
+
|
|
96
|
+
Only ticks metrics for failing, leaf spans (isFailureSource).
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
span: The span to record telemetry for.
|
|
100
|
+
log_input_and_output: Whether to log input/output (unused here).
|
|
101
|
+
project_id: Optional GCP project ID.
|
|
102
|
+
"""
|
|
103
|
+
attrs = span.attributes or {}
|
|
104
|
+
|
|
105
|
+
path = str(attrs.get('genkit:path', ''))
|
|
106
|
+
is_failure_source = bool(attrs.get('genkit:isFailureSource'))
|
|
107
|
+
state = str(attrs.get('genkit:state', ''))
|
|
108
|
+
|
|
109
|
+
# Only tick metrics for failing, leaf spans
|
|
110
|
+
if not path or not is_failure_source or state != 'error':
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
session_id = str(attrs.get('genkit:sessionId', '')) or None
|
|
114
|
+
thread_name = str(attrs.get('genkit:threadName', '')) or None
|
|
115
|
+
|
|
116
|
+
events = list(span.events)
|
|
117
|
+
error_name = extract_error_name(events) or '<unknown>'
|
|
118
|
+
error_message = extract_error_message(events) or '<unknown>'
|
|
119
|
+
error_stack = extract_error_stack(events) or ''
|
|
120
|
+
|
|
121
|
+
# Calculate latency
|
|
122
|
+
latency_ms = 0.0
|
|
123
|
+
if span.end_time and span.start_time:
|
|
124
|
+
latency_ms = (span.end_time - span.start_time) / 1_000_000
|
|
125
|
+
|
|
126
|
+
path_dimensions = {
|
|
127
|
+
'featureName': extract_outer_feature_name_from_path(path)[:256],
|
|
128
|
+
'status': 'failure',
|
|
129
|
+
'error': error_name[:256],
|
|
130
|
+
'path': path[:256],
|
|
131
|
+
'source': 'py',
|
|
132
|
+
'sourceVersion': GENKIT_VERSION,
|
|
133
|
+
}
|
|
134
|
+
_get_path_counter().add(1, path_dimensions)
|
|
135
|
+
_get_path_latency().record(latency_ms, path_dimensions)
|
|
136
|
+
|
|
137
|
+
display_path = truncate_path(to_display_path(path))
|
|
138
|
+
log_attrs = {
|
|
139
|
+
**create_common_log_attributes(span, project_id),
|
|
140
|
+
'path': display_path,
|
|
141
|
+
'qualifiedPath': path,
|
|
142
|
+
'name': error_name,
|
|
143
|
+
'message': error_message,
|
|
144
|
+
'stack': error_stack,
|
|
145
|
+
'source': 'py',
|
|
146
|
+
'sourceVersion': GENKIT_VERSION,
|
|
147
|
+
}
|
|
148
|
+
if session_id:
|
|
149
|
+
log_attrs['sessionId'] = session_id
|
|
150
|
+
if thread_name:
|
|
151
|
+
log_attrs['threadName'] = thread_name
|
|
152
|
+
|
|
153
|
+
logger.error(f'Error[{display_path}, {error_name}]', **log_attrs)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# Singleton instance
|
|
157
|
+
paths_telemetry = PathsTelemetry()
|