@frengki0707/google-cloud-clone 1.33.0
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.
- package/LICENSE +203 -0
- package/README.md +83 -0
- package/lib/auth.d.mts +33 -0
- package/lib/auth.d.ts +33 -0
- package/lib/auth.js +70 -0
- package/lib/auth.js.map +1 -0
- package/lib/auth.mjs +45 -0
- package/lib/auth.mjs.map +1 -0
- package/lib/gcpLogger.d.mts +25 -0
- package/lib/gcpLogger.d.ts +25 -0
- package/lib/gcpLogger.js +118 -0
- package/lib/gcpLogger.js.map +1 -0
- package/lib/gcpLogger.mjs +82 -0
- package/lib/gcpLogger.mjs.map +1 -0
- package/lib/gcpOpenTelemetry.d.mts +59 -0
- package/lib/gcpOpenTelemetry.d.ts +59 -0
- package/lib/gcpOpenTelemetry.js +374 -0
- package/lib/gcpOpenTelemetry.js.map +1 -0
- package/lib/gcpOpenTelemetry.mjs +364 -0
- package/lib/gcpOpenTelemetry.mjs.map +1 -0
- package/lib/index.d.mts +36 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.js +56 -0
- package/lib/index.js.map +1 -0
- package/lib/index.mjs +29 -0
- package/lib/index.mjs.map +1 -0
- package/lib/metrics.d.mts +65 -0
- package/lib/metrics.d.ts +65 -0
- package/lib/metrics.js +91 -0
- package/lib/metrics.js.map +1 -0
- package/lib/metrics.mjs +65 -0
- package/lib/metrics.mjs.map +1 -0
- package/lib/model-armor.d.mts +59 -0
- package/lib/model-armor.d.ts +59 -0
- package/lib/model-armor.js +205 -0
- package/lib/model-armor.js.map +1 -0
- package/lib/model-armor.mjs +181 -0
- package/lib/model-armor.mjs.map +1 -0
- package/lib/telemetry/action.d.mts +27 -0
- package/lib/telemetry/action.d.ts +27 -0
- package/lib/telemetry/action.js +92 -0
- package/lib/telemetry/action.js.map +1 -0
- package/lib/telemetry/action.mjs +73 -0
- package/lib/telemetry/action.mjs.map +1 -0
- package/lib/telemetry/defaults.d.mts +30 -0
- package/lib/telemetry/defaults.d.ts +30 -0
- package/lib/telemetry/defaults.js +70 -0
- package/lib/telemetry/defaults.js.map +1 -0
- package/lib/telemetry/defaults.mjs +46 -0
- package/lib/telemetry/defaults.mjs.map +1 -0
- package/lib/telemetry/engagement.d.mts +35 -0
- package/lib/telemetry/engagement.d.ts +35 -0
- package/lib/telemetry/engagement.js +106 -0
- package/lib/telemetry/engagement.js.map +1 -0
- package/lib/telemetry/engagement.mjs +85 -0
- package/lib/telemetry/engagement.mjs.map +1 -0
- package/lib/telemetry/feature.d.mts +35 -0
- package/lib/telemetry/feature.d.ts +35 -0
- package/lib/telemetry/feature.js +142 -0
- package/lib/telemetry/feature.js.map +1 -0
- package/lib/telemetry/feature.mjs +127 -0
- package/lib/telemetry/feature.mjs.map +1 -0
- package/lib/telemetry/generate.d.mts +53 -0
- package/lib/telemetry/generate.d.ts +53 -0
- package/lib/telemetry/generate.js +326 -0
- package/lib/telemetry/generate.js.map +1 -0
- package/lib/telemetry/generate.mjs +314 -0
- package/lib/telemetry/generate.mjs.map +1 -0
- package/lib/telemetry/path.d.mts +32 -0
- package/lib/telemetry/path.d.ts +32 -0
- package/lib/telemetry/path.js +91 -0
- package/lib/telemetry/path.js.map +1 -0
- package/lib/telemetry/path.mjs +78 -0
- package/lib/telemetry/path.mjs.map +1 -0
- package/lib/types.d.mts +121 -0
- package/lib/types.d.ts +121 -0
- package/lib/types.js +17 -0
- package/lib/types.js.map +1 -0
- package/lib/types.mjs +1 -0
- package/lib/types.mjs.map +1 -0
- package/lib/utils.d.mts +57 -0
- package/lib/utils.d.ts +57 -0
- package/lib/utils.js +143 -0
- package/lib/utils.js.map +1 -0
- package/lib/utils.mjs +104 -0
- package/lib/utils.mjs.map +1 -0
- package/package.json +89 -0
- package/src/auth.ts +89 -0
- package/src/gcpLogger.ts +124 -0
- package/src/gcpOpenTelemetry.ts +485 -0
- package/src/index.ts +59 -0
- package/src/metrics.ts +122 -0
- package/src/model-armor.ts +317 -0
- package/src/telemetry/action.ts +106 -0
- package/src/telemetry/defaults.ts +72 -0
- package/src/telemetry/engagement.ts +120 -0
- package/src/telemetry/feature.ts +170 -0
- package/src/telemetry/generate.ts +454 -0
- package/src/telemetry/path.ts +111 -0
- package/src/types.ts +133 -0
- package/src/utils.ts +175 -0
- package/tests/logs_no_input_output_test.ts +267 -0
- package/tests/logs_session_test.ts +219 -0
- package/tests/logs_test.ts +633 -0
- package/tests/metrics_test.ts +792 -0
- package/tests/model_armor_test.ts +336 -0
- package/tests/traces_test.ts +380 -0
- package/typedoc.json +3 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2025 Google LLC
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { ModelArmorClient, protos } from '@google-cloud/modelarmor';
|
|
18
|
+
import { GenkitError } from 'genkit';
|
|
19
|
+
import {
|
|
20
|
+
GenerateRequest,
|
|
21
|
+
GenerateResponseData,
|
|
22
|
+
MessageData,
|
|
23
|
+
ModelMiddleware,
|
|
24
|
+
Part,
|
|
25
|
+
} from 'genkit/model';
|
|
26
|
+
import { runInNewSpan } from 'genkit/tracing';
|
|
27
|
+
|
|
28
|
+
export interface ModelArmorOptions {
|
|
29
|
+
templateName: string;
|
|
30
|
+
client?: ModelArmorClient;
|
|
31
|
+
/**
|
|
32
|
+
* Options for the Model Armor client (e.g. apiEndpoint).
|
|
33
|
+
*/
|
|
34
|
+
clientOptions?: ConstructorParameters<typeof ModelArmorClient>[0];
|
|
35
|
+
/**
|
|
36
|
+
* What to sanitize. Defaults to 'all'.
|
|
37
|
+
*/
|
|
38
|
+
protectionTarget?: 'all' | 'userPrompt' | 'modelResponse';
|
|
39
|
+
/**
|
|
40
|
+
* Whether to block on SDP match even if the content was successfully de-identified.
|
|
41
|
+
* Defaults to false (lenient).
|
|
42
|
+
*/
|
|
43
|
+
strictSdpEnforcement?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* List of filters to enforce. If not specified, all filters are enforced.
|
|
46
|
+
* Possible values: 'rai', 'pi_and_jailbreak', 'malicious_uris', 'csam', 'sdp'.
|
|
47
|
+
*/
|
|
48
|
+
filters?: (
|
|
49
|
+
| 'rai'
|
|
50
|
+
| 'pi_and_jailbreak'
|
|
51
|
+
| 'malicious_uris'
|
|
52
|
+
| 'csam'
|
|
53
|
+
| 'sdp'
|
|
54
|
+
| (string & {})
|
|
55
|
+
)[];
|
|
56
|
+
/**
|
|
57
|
+
* Whether to apply the de-identification results to the content.
|
|
58
|
+
* - If true, the default logic (replace text, preserve structure) is used.
|
|
59
|
+
* - If false, no changes are applied.
|
|
60
|
+
* - If a function, it is called with the messages and SDP result, and should return the new messages.
|
|
61
|
+
*
|
|
62
|
+
* Defaults to false.
|
|
63
|
+
*/
|
|
64
|
+
applyDeidentificationResults?:
|
|
65
|
+
| boolean
|
|
66
|
+
| ((data: {
|
|
67
|
+
messages: MessageData[];
|
|
68
|
+
sdpResult: protos.google.cloud.modelarmor.v1.ISdpFilterResult;
|
|
69
|
+
}) => MessageData[] | undefined);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractText(parts: Part[]): string {
|
|
73
|
+
return parts.map((p) => p.text || '').join('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* If SDP (Sensitive Data Protection) filter returns sanitized data,
|
|
78
|
+
* we swap out the data with sanitized data.
|
|
79
|
+
*/
|
|
80
|
+
function applySdp(
|
|
81
|
+
messages: MessageData[],
|
|
82
|
+
targetIndex: number,
|
|
83
|
+
result: protos.google.cloud.modelarmor.v1.ISanitizationResult,
|
|
84
|
+
options: ModelArmorOptions
|
|
85
|
+
): { sdpApplied: boolean; messages: MessageData[] } {
|
|
86
|
+
const sdpFilterResult = result.filterResults?.['sdp']?.sdpFilterResult;
|
|
87
|
+
|
|
88
|
+
if (!sdpFilterResult) {
|
|
89
|
+
return { sdpApplied: false, messages };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// If user provided applyDeidentificationResults, we use it to apply
|
|
93
|
+
// the deidentification results.
|
|
94
|
+
if (typeof options.applyDeidentificationResults === 'function') {
|
|
95
|
+
const newMessages = options.applyDeidentificationResults({
|
|
96
|
+
messages,
|
|
97
|
+
sdpResult: sdpFilterResult,
|
|
98
|
+
});
|
|
99
|
+
if (!newMessages) {
|
|
100
|
+
return { sdpApplied: false, messages };
|
|
101
|
+
}
|
|
102
|
+
const sdpApplied = !!sdpFilterResult.deidentifyResult?.data?.text;
|
|
103
|
+
return { sdpApplied, messages: newMessages };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// if applyDeidentificationResults is set to true, we use the default/basic
|
|
107
|
+
// approach to apply the results.
|
|
108
|
+
if (options.applyDeidentificationResults === true) {
|
|
109
|
+
const deidentifyResult = sdpFilterResult.deidentifyResult;
|
|
110
|
+
if (deidentifyResult && deidentifyResult.data?.text) {
|
|
111
|
+
const targetMessage = messages[targetIndex];
|
|
112
|
+
const nonTextParts = targetMessage.content.filter((p) => !p.text);
|
|
113
|
+
const newContent = [
|
|
114
|
+
...nonTextParts,
|
|
115
|
+
{ text: deidentifyResult.data.text },
|
|
116
|
+
];
|
|
117
|
+
const newMessages = [...messages];
|
|
118
|
+
newMessages[targetIndex] = { ...targetMessage, content: newContent };
|
|
119
|
+
return {
|
|
120
|
+
sdpApplied: true,
|
|
121
|
+
messages: newMessages,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { sdpApplied: false, messages };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function shouldBlock(
|
|
130
|
+
result: protos.google.cloud.modelarmor.v1.ISanitizationResult,
|
|
131
|
+
options: ModelArmorOptions,
|
|
132
|
+
sdpApplied: boolean
|
|
133
|
+
): boolean {
|
|
134
|
+
if (result.filterMatchState !== 'MATCH_FOUND') {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
// Check if we should block.
|
|
138
|
+
// If strict SDP enforcement is enabled and SDP was applied, we must block.
|
|
139
|
+
if (options.strictSdpEnforcement && sdpApplied) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
// Otherwise, check if any active filter matched.
|
|
143
|
+
if (result.filterResults) {
|
|
144
|
+
for (const [key, filterResult] of Object.entries(result.filterResults)) {
|
|
145
|
+
if (options.filters && !options.filters.includes(key)) continue;
|
|
146
|
+
if (key === 'sdp' && sdpApplied) continue;
|
|
147
|
+
|
|
148
|
+
// Look for matchState in the nested object
|
|
149
|
+
// e.g. filterResult.raiFilterResult.matchState
|
|
150
|
+
const nestedResult = Object.values(filterResult)[0];
|
|
151
|
+
if (nestedResult?.matchState === 'MATCH_FOUND') {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function sanitizeUserPrompt(
|
|
160
|
+
req: GenerateRequest,
|
|
161
|
+
client: ModelArmorClient,
|
|
162
|
+
options: ModelArmorOptions
|
|
163
|
+
) {
|
|
164
|
+
let targetMessageIndex = -1;
|
|
165
|
+
// Find the last user message to sanitize
|
|
166
|
+
for (let i = req.messages.length - 1; i >= 0; i--) {
|
|
167
|
+
if (req.messages[i].role === 'user') {
|
|
168
|
+
targetMessageIndex = i;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (targetMessageIndex !== -1) {
|
|
174
|
+
const userMessage = req.messages[targetMessageIndex];
|
|
175
|
+
const promptText = extractText(userMessage.content);
|
|
176
|
+
|
|
177
|
+
if (promptText) {
|
|
178
|
+
await runInNewSpan(
|
|
179
|
+
{ metadata: { name: 'sanitizeUserPrompt' } },
|
|
180
|
+
async (meta) => {
|
|
181
|
+
meta.input = {
|
|
182
|
+
name: options.templateName,
|
|
183
|
+
userPromptData: {
|
|
184
|
+
text: promptText,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
const [response] = await client.sanitizeUserPrompt({
|
|
188
|
+
name: options.templateName,
|
|
189
|
+
userPromptData: {
|
|
190
|
+
text: promptText,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
meta.output = response;
|
|
194
|
+
|
|
195
|
+
if (response.sanitizationResult) {
|
|
196
|
+
const result = response.sanitizationResult;
|
|
197
|
+
const { sdpApplied, messages: modifiedMessages } = applySdp(
|
|
198
|
+
req.messages,
|
|
199
|
+
targetMessageIndex,
|
|
200
|
+
result,
|
|
201
|
+
options
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
if (
|
|
205
|
+
sdpApplied ||
|
|
206
|
+
typeof options.applyDeidentificationResults === 'function'
|
|
207
|
+
) {
|
|
208
|
+
req.messages = modifiedMessages;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (shouldBlock(result, options, sdpApplied)) {
|
|
212
|
+
throw new GenkitError({
|
|
213
|
+
status: 'PERMISSION_DENIED',
|
|
214
|
+
message: 'Model Armor blocked user prompt.',
|
|
215
|
+
detail: result,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function sanitizeModelResponse(
|
|
226
|
+
response: GenerateResponseData,
|
|
227
|
+
client: ModelArmorClient,
|
|
228
|
+
options: ModelArmorOptions
|
|
229
|
+
) {
|
|
230
|
+
const usingMessageProp = !!response.message;
|
|
231
|
+
const candidates = response.message
|
|
232
|
+
? [{ index: 0, message: response.message, finishReason: 'stop' }]
|
|
233
|
+
: response.candidates || [];
|
|
234
|
+
|
|
235
|
+
for (const candidate of candidates) {
|
|
236
|
+
const modelText = extractText(candidate.message.content);
|
|
237
|
+
|
|
238
|
+
if (modelText) {
|
|
239
|
+
await runInNewSpan(
|
|
240
|
+
{ metadata: { name: 'sanitizeModelResponse' } },
|
|
241
|
+
async (meta) => {
|
|
242
|
+
meta.input = {
|
|
243
|
+
name: options.templateName,
|
|
244
|
+
modelResponseData: {
|
|
245
|
+
text: modelText,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
const [apiResponse] = await client.sanitizeModelResponse({
|
|
249
|
+
name: options.templateName,
|
|
250
|
+
modelResponseData: {
|
|
251
|
+
text: modelText,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
meta.output = apiResponse;
|
|
255
|
+
|
|
256
|
+
if (apiResponse.sanitizationResult) {
|
|
257
|
+
const result = apiResponse.sanitizationResult;
|
|
258
|
+
const { sdpApplied, messages: modifiedMessages } = applySdp(
|
|
259
|
+
[candidate.message],
|
|
260
|
+
0,
|
|
261
|
+
result,
|
|
262
|
+
options
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (
|
|
266
|
+
sdpApplied ||
|
|
267
|
+
typeof options.applyDeidentificationResults === 'function'
|
|
268
|
+
) {
|
|
269
|
+
candidate.message = modifiedMessages[0];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (shouldBlock(result, options, sdpApplied)) {
|
|
273
|
+
throw new GenkitError({
|
|
274
|
+
status: 'PERMISSION_DENIED',
|
|
275
|
+
message: 'Model Armor blocked model response.',
|
|
276
|
+
detail: result,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (usingMessageProp && candidates.length > 0) {
|
|
286
|
+
response.message = candidates[0].message;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Model Middleware that uses Google Cloud Model Armor to sanitize user prompts and model responses.
|
|
292
|
+
*/
|
|
293
|
+
export function modelArmor(options: ModelArmorOptions): ModelMiddleware {
|
|
294
|
+
const client = options.client || new ModelArmorClient(options.clientOptions);
|
|
295
|
+
const protectionTarget = options.protectionTarget ?? 'all';
|
|
296
|
+
const protectUserPrompt =
|
|
297
|
+
protectionTarget === 'all' || protectionTarget === 'userPrompt';
|
|
298
|
+
const protectModelResponse =
|
|
299
|
+
protectionTarget === 'all' || protectionTarget === 'modelResponse';
|
|
300
|
+
|
|
301
|
+
return async (req, next) => {
|
|
302
|
+
// 1. Sanitize User Prompt
|
|
303
|
+
if (protectUserPrompt) {
|
|
304
|
+
await sanitizeUserPrompt(req, client, options);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 2. Call Model
|
|
308
|
+
const response = await next(req);
|
|
309
|
+
|
|
310
|
+
// 3. Sanitize Model Response
|
|
311
|
+
if (protectModelResponse) {
|
|
312
|
+
await sanitizeModelResponse(response, client, options);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return response;
|
|
316
|
+
};
|
|
317
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2024 Google LLC
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
18
|
+
import { logger } from 'genkit/logging';
|
|
19
|
+
import { toDisplayPath } from 'genkit/tracing';
|
|
20
|
+
import { type Telemetry } from '../metrics.js';
|
|
21
|
+
import {
|
|
22
|
+
createCommonLogAttributes,
|
|
23
|
+
extractOuterFeatureNameFromPath,
|
|
24
|
+
truncate,
|
|
25
|
+
truncatePath,
|
|
26
|
+
} from '../utils.js';
|
|
27
|
+
|
|
28
|
+
class ActionTelemetry implements Telemetry {
|
|
29
|
+
tick(
|
|
30
|
+
span: ReadableSpan,
|
|
31
|
+
logInputAndOutput: boolean,
|
|
32
|
+
projectId?: string
|
|
33
|
+
): void {
|
|
34
|
+
if (!logInputAndOutput) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const attributes = span.attributes;
|
|
38
|
+
const actionName = (attributes['genkit:name'] as string) || '<unknown>';
|
|
39
|
+
const subtype = attributes['genkit:metadata:subtype'] as string;
|
|
40
|
+
|
|
41
|
+
if (subtype === 'tool' || actionName === 'generate') {
|
|
42
|
+
const path = (attributes['genkit:path'] as string) || '<unknown>';
|
|
43
|
+
const input = truncate(attributes['genkit:input'] as string);
|
|
44
|
+
const output = truncate(attributes['genkit:output'] as string);
|
|
45
|
+
const sessionId = attributes['genkit:sessionId'] as string;
|
|
46
|
+
const threadName = attributes['genkit:threadName'] as string;
|
|
47
|
+
let featureName = extractOuterFeatureNameFromPath(path);
|
|
48
|
+
if (!featureName || featureName === '<unknown>') {
|
|
49
|
+
featureName = actionName;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (input) {
|
|
53
|
+
this.writeLog(
|
|
54
|
+
span,
|
|
55
|
+
'Input',
|
|
56
|
+
featureName,
|
|
57
|
+
path,
|
|
58
|
+
input,
|
|
59
|
+
projectId,
|
|
60
|
+
sessionId,
|
|
61
|
+
threadName
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (output) {
|
|
65
|
+
this.writeLog(
|
|
66
|
+
span,
|
|
67
|
+
'Output',
|
|
68
|
+
featureName,
|
|
69
|
+
path,
|
|
70
|
+
output,
|
|
71
|
+
projectId,
|
|
72
|
+
sessionId,
|
|
73
|
+
threadName
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private writeLog(
|
|
80
|
+
span: ReadableSpan,
|
|
81
|
+
tag: string,
|
|
82
|
+
featureName: string,
|
|
83
|
+
qualifiedPath: string,
|
|
84
|
+
content: string,
|
|
85
|
+
projectId?: string,
|
|
86
|
+
sessionId?: string,
|
|
87
|
+
threadName?: string
|
|
88
|
+
) {
|
|
89
|
+
const path = truncatePath(toDisplayPath(qualifiedPath));
|
|
90
|
+
const sharedMetadata = {
|
|
91
|
+
...createCommonLogAttributes(span, projectId),
|
|
92
|
+
path,
|
|
93
|
+
qualifiedPath,
|
|
94
|
+
featureName,
|
|
95
|
+
sessionId,
|
|
96
|
+
threadName,
|
|
97
|
+
};
|
|
98
|
+
logger.logStructured(`${tag}[${path}, ${featureName}]`, {
|
|
99
|
+
...sharedMetadata,
|
|
100
|
+
content,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const actionTelemetry = new ActionTelemetry();
|
|
106
|
+
export { actionTelemetry };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2024 Google LLC
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { AlwaysOnSampler } from '@opentelemetry/sdk-trace-base';
|
|
18
|
+
import { isDevEnv } from 'genkit';
|
|
19
|
+
import type {
|
|
20
|
+
GcpTelemetryConfig,
|
|
21
|
+
GcpTelemetryConfigOptions,
|
|
22
|
+
} from '../types.js';
|
|
23
|
+
|
|
24
|
+
/** Consolidated defaults for telemetry configuration. */
|
|
25
|
+
|
|
26
|
+
export const TelemetryConfigs = {
|
|
27
|
+
defaults: (overrides: GcpTelemetryConfigOptions = {}): GcpTelemetryConfig => {
|
|
28
|
+
return isDevEnv()
|
|
29
|
+
? TelemetryConfigs.developmentDefaults(overrides)
|
|
30
|
+
: TelemetryConfigs.productionDefaults(overrides);
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
developmentDefaults: (
|
|
34
|
+
overrides: GcpTelemetryConfigOptions = {}
|
|
35
|
+
): GcpTelemetryConfig => {
|
|
36
|
+
const defaults = {
|
|
37
|
+
sampler: new AlwaysOnSampler(),
|
|
38
|
+
autoInstrumentation: true,
|
|
39
|
+
autoInstrumentationConfig: {
|
|
40
|
+
'@opentelemetry/instrumentation-dns': { enabled: false },
|
|
41
|
+
},
|
|
42
|
+
instrumentations: [],
|
|
43
|
+
metricExportIntervalMillis: 5_000,
|
|
44
|
+
metricExportTimeoutMillis: 5_000,
|
|
45
|
+
disableMetrics: false,
|
|
46
|
+
disableTraces: false,
|
|
47
|
+
exportInputAndOutput: !overrides.disableLoggingInputAndOutput,
|
|
48
|
+
export: !!overrides.forceDevExport, // false
|
|
49
|
+
};
|
|
50
|
+
return { ...defaults, ...overrides };
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
productionDefaults: (
|
|
54
|
+
overrides: GcpTelemetryConfigOptions = {}
|
|
55
|
+
): GcpTelemetryConfig => {
|
|
56
|
+
const defaults = {
|
|
57
|
+
sampler: new AlwaysOnSampler(),
|
|
58
|
+
autoInstrumentation: true,
|
|
59
|
+
autoInstrumentationConfig: {
|
|
60
|
+
'@opentelemetry/instrumentation-dns': { enabled: false },
|
|
61
|
+
},
|
|
62
|
+
instrumentations: [],
|
|
63
|
+
metricExportIntervalMillis: 300_000,
|
|
64
|
+
metricExportTimeoutMillis: 300_000,
|
|
65
|
+
disableMetrics: false,
|
|
66
|
+
disableTraces: false,
|
|
67
|
+
exportInputAndOutput: !overrides.disableLoggingInputAndOutput,
|
|
68
|
+
export: true,
|
|
69
|
+
};
|
|
70
|
+
return { ...defaults, ...overrides };
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2024 Google LLC
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { ValueType, type Attributes } from '@opentelemetry/api';
|
|
18
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
19
|
+
import { GENKIT_VERSION } from 'genkit';
|
|
20
|
+
import { logger } from 'genkit/logging';
|
|
21
|
+
import {
|
|
22
|
+
MetricCounter,
|
|
23
|
+
internalMetricNamespaceWrap,
|
|
24
|
+
type Telemetry,
|
|
25
|
+
} from '../metrics.js';
|
|
26
|
+
import { createCommonLogAttributes, truncate } from '../utils.js';
|
|
27
|
+
|
|
28
|
+
class EngagementTelemetry implements Telemetry {
|
|
29
|
+
/**
|
|
30
|
+
* Wraps the declared metrics in a Genkit-specific, internal namespace.
|
|
31
|
+
*/
|
|
32
|
+
private _N = internalMetricNamespaceWrap.bind(null, 'engagement');
|
|
33
|
+
|
|
34
|
+
private feedbackCounter = new MetricCounter(this._N('feedback'), {
|
|
35
|
+
description: 'Counts calls to genkit flows.',
|
|
36
|
+
valueType: ValueType.INT,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
private acceptanceCounter = new MetricCounter(this._N('acceptance'), {
|
|
40
|
+
description: 'Tracks unique flow paths per flow.',
|
|
41
|
+
valueType: ValueType.INT,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
tick(
|
|
45
|
+
span: ReadableSpan,
|
|
46
|
+
logInputAndOutput: boolean,
|
|
47
|
+
projectId?: string
|
|
48
|
+
): void {
|
|
49
|
+
const subtype = span.attributes['genkit:metadata:subtype'] as string;
|
|
50
|
+
|
|
51
|
+
if (subtype === 'userFeedback') {
|
|
52
|
+
this.writeUserFeedback(span, projectId);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (subtype === 'userAcceptance') {
|
|
57
|
+
this.writeUserAcceptance(span, projectId);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
logger.warn(`Unknown user engagement subtype: ${subtype}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private writeUserFeedback(span: ReadableSpan, projectId?: string) {
|
|
65
|
+
const attributes = span.attributes;
|
|
66
|
+
const name = this.extractTraceName(attributes);
|
|
67
|
+
|
|
68
|
+
const dimensions = {
|
|
69
|
+
name,
|
|
70
|
+
value: attributes['genkit:metadata:feedbackValue'],
|
|
71
|
+
hasText: !!attributes['genkit:metadata:textFeedback'],
|
|
72
|
+
source: 'ts',
|
|
73
|
+
sourceVersion: GENKIT_VERSION,
|
|
74
|
+
};
|
|
75
|
+
this.feedbackCounter.add(1, dimensions);
|
|
76
|
+
|
|
77
|
+
const metadata = {
|
|
78
|
+
...createCommonLogAttributes(span, projectId),
|
|
79
|
+
feedbackValue: attributes['genkit:metadata:feedbackValue'],
|
|
80
|
+
};
|
|
81
|
+
if (attributes['genkit:metadata:textFeedback']) {
|
|
82
|
+
metadata['textFeedback'] = truncate(
|
|
83
|
+
attributes['genkit:metadata:textFeedback'] as string
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
logger.logStructured(`UserFeedback[${name}]`, metadata);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private writeUserAcceptance(span: ReadableSpan, projectId?: string) {
|
|
90
|
+
const attributes = span.attributes;
|
|
91
|
+
const name = this.extractTraceName(attributes);
|
|
92
|
+
|
|
93
|
+
const dimensions = {
|
|
94
|
+
name,
|
|
95
|
+
value: attributes['genkit:metadata:acceptanceValue'],
|
|
96
|
+
source: 'ts',
|
|
97
|
+
sourceVersion: GENKIT_VERSION,
|
|
98
|
+
};
|
|
99
|
+
this.acceptanceCounter.add(1, dimensions);
|
|
100
|
+
|
|
101
|
+
const metadata = {
|
|
102
|
+
...createCommonLogAttributes(span, projectId),
|
|
103
|
+
acceptanceValue: attributes['genkit:metadata:acceptanceValue'],
|
|
104
|
+
};
|
|
105
|
+
logger.logStructured(`UserAcceptance[${name}]`, metadata);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private extractTraceName(attributes: Attributes) {
|
|
109
|
+
const path = attributes['genkit:path'] as string;
|
|
110
|
+
if (!path || path === '<unknown>') {
|
|
111
|
+
return '<unknown>';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const name = path.match('/{(.+)}+');
|
|
115
|
+
return name ? name[1] : '<unknown>';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const engagementTelemetry = new EngagementTelemetry();
|
|
120
|
+
export { engagementTelemetry };
|