google-logging-utils-internal 1.1.3
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/package/LICENSE +203 -0
- package/package/README.md +83 -0
- package/package/lib/auth.d.mts +33 -0
- package/package/lib/auth.d.ts +33 -0
- package/package/lib/auth.js +70 -0
- package/package/lib/auth.js.map +1 -0
- package/package/lib/auth.mjs +45 -0
- package/package/lib/auth.mjs.map +1 -0
- package/package/lib/gcpLogger.d.mts +25 -0
- package/package/lib/gcpLogger.d.ts +25 -0
- package/package/lib/gcpLogger.js +118 -0
- package/package/lib/gcpLogger.js.map +1 -0
- package/package/lib/gcpLogger.mjs +82 -0
- package/package/lib/gcpLogger.mjs.map +1 -0
- package/package/lib/gcpOpenTelemetry.d.mts +59 -0
- package/package/lib/gcpOpenTelemetry.d.ts +59 -0
- package/package/lib/gcpOpenTelemetry.js +374 -0
- package/package/lib/gcpOpenTelemetry.js.map +1 -0
- package/package/lib/gcpOpenTelemetry.mjs +364 -0
- package/package/lib/gcpOpenTelemetry.mjs.map +1 -0
- package/package/lib/index.d.mts +36 -0
- package/package/lib/index.d.ts +36 -0
- package/package/lib/index.js +56 -0
- package/package/lib/index.js.map +1 -0
- package/package/lib/index.mjs +29 -0
- package/package/lib/index.mjs.map +1 -0
- package/package/lib/metrics.d.mts +65 -0
- package/package/lib/metrics.d.ts +65 -0
- package/package/lib/metrics.js +91 -0
- package/package/lib/metrics.js.map +1 -0
- package/package/lib/metrics.mjs +65 -0
- package/package/lib/metrics.mjs.map +1 -0
- package/package/lib/model-armor.d.mts +59 -0
- package/package/lib/model-armor.d.ts +59 -0
- package/package/lib/model-armor.js +205 -0
- package/package/lib/model-armor.js.map +1 -0
- package/package/lib/model-armor.mjs +181 -0
- package/package/lib/model-armor.mjs.map +1 -0
- package/package/lib/telemetry/action.d.mts +27 -0
- package/package/lib/telemetry/action.d.ts +27 -0
- package/package/lib/telemetry/action.js +92 -0
- package/package/lib/telemetry/action.js.map +1 -0
- package/package/lib/telemetry/action.mjs +73 -0
- package/package/lib/telemetry/action.mjs.map +1 -0
- package/package/lib/telemetry/defaults.d.mts +30 -0
- package/package/lib/telemetry/defaults.d.ts +30 -0
- package/package/lib/telemetry/defaults.js +70 -0
- package/package/lib/telemetry/defaults.js.map +1 -0
- package/package/lib/telemetry/defaults.mjs +46 -0
- package/package/lib/telemetry/defaults.mjs.map +1 -0
- package/package/lib/telemetry/engagement.d.mts +35 -0
- package/package/lib/telemetry/engagement.d.ts +35 -0
- package/package/lib/telemetry/engagement.js +106 -0
- package/package/lib/telemetry/engagement.js.map +1 -0
- package/package/lib/telemetry/engagement.mjs +85 -0
- package/package/lib/telemetry/engagement.mjs.map +1 -0
- package/package/lib/telemetry/feature.d.mts +35 -0
- package/package/lib/telemetry/feature.d.ts +35 -0
- package/package/lib/telemetry/feature.js +142 -0
- package/package/lib/telemetry/feature.js.map +1 -0
- package/package/lib/telemetry/feature.mjs +127 -0
- package/package/lib/telemetry/feature.mjs.map +1 -0
- package/package/lib/telemetry/generate.d.mts +53 -0
- package/package/lib/telemetry/generate.d.ts +53 -0
- package/package/lib/telemetry/generate.js +326 -0
- package/package/lib/telemetry/generate.js.map +1 -0
- package/package/lib/telemetry/generate.mjs +314 -0
- package/package/lib/telemetry/generate.mjs.map +1 -0
- package/package/lib/telemetry/path.d.mts +32 -0
- package/package/lib/telemetry/path.d.ts +32 -0
- package/package/lib/telemetry/path.js +91 -0
- package/package/lib/telemetry/path.js.map +1 -0
- package/package/lib/telemetry/path.mjs +78 -0
- package/package/lib/telemetry/path.mjs.map +1 -0
- package/package/lib/types.d.mts +121 -0
- package/package/lib/types.d.ts +121 -0
- package/package/lib/types.js +17 -0
- package/package/lib/types.js.map +1 -0
- package/package/lib/types.mjs +1 -0
- package/package/lib/types.mjs.map +1 -0
- package/package/lib/utils.d.mts +57 -0
- package/package/lib/utils.d.ts +57 -0
- package/package/lib/utils.js +143 -0
- package/package/lib/utils.js.map +1 -0
- package/package/lib/utils.mjs +104 -0
- package/package/lib/utils.mjs.map +1 -0
- package/package/package.json +90 -0
- package/package/src/auth.ts +89 -0
- package/package/src/gcpLogger.ts +124 -0
- package/package/src/gcpOpenTelemetry.ts +485 -0
- package/package/src/index.ts +59 -0
- package/package/src/metrics.ts +122 -0
- package/package/src/model-armor.ts +317 -0
- package/package/src/telemetry/action.ts +106 -0
- package/package/src/telemetry/defaults.ts +72 -0
- package/package/src/telemetry/engagement.ts +120 -0
- package/package/src/telemetry/feature.ts +170 -0
- package/package/src/telemetry/generate.ts +454 -0
- package/package/src/telemetry/path.ts +111 -0
- package/package/src/types.ts +133 -0
- package/package/src/utils.ts +175 -0
- package/package/tests/logs_no_input_output_test.ts +267 -0
- package/package/tests/logs_session_test.ts +219 -0
- package/package/tests/logs_test.ts +633 -0
- package/package/tests/metrics_test.ts +792 -0
- package/package/tests/model_armor_test.ts +336 -0
- package/package/tests/traces_test.ts +380 -0
- package/package/typedoc.json +3 -0
- package/package.json +10 -0
|
@@ -0,0 +1,633 @@
|
|
|
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 {
|
|
18
|
+
afterAll,
|
|
19
|
+
beforeAll,
|
|
20
|
+
beforeEach,
|
|
21
|
+
describe,
|
|
22
|
+
expect,
|
|
23
|
+
it,
|
|
24
|
+
jest,
|
|
25
|
+
} from '@jest/globals';
|
|
26
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
27
|
+
import * as assert from 'assert';
|
|
28
|
+
import { genkit, z, type GenerateResponseData, type Genkit } from 'genkit';
|
|
29
|
+
import { SPAN_TYPE_ATTR, appendSpan } from 'genkit/tracing';
|
|
30
|
+
import { Writable } from 'stream';
|
|
31
|
+
import {
|
|
32
|
+
__addTransportStreamForTesting,
|
|
33
|
+
__forceFlushSpansForTesting,
|
|
34
|
+
__getSpanExporterForTesting,
|
|
35
|
+
__useJsonFormatForTesting,
|
|
36
|
+
enableGoogleCloudTelemetry,
|
|
37
|
+
} from '../src/index.js';
|
|
38
|
+
|
|
39
|
+
jest.mock('../src/auth.js', () => {
|
|
40
|
+
const original = jest.requireActual('../src/auth.js');
|
|
41
|
+
return {
|
|
42
|
+
...(original || {}),
|
|
43
|
+
resolveCurrentPrincipal: jest.fn().mockImplementation(() => {
|
|
44
|
+
return Promise.resolve({
|
|
45
|
+
projectId: 'test',
|
|
46
|
+
serviceAccountEmail: 'test@test.com',
|
|
47
|
+
});
|
|
48
|
+
}),
|
|
49
|
+
credentialsFromEnvironment: jest.fn().mockImplementation(() => {
|
|
50
|
+
return Promise.resolve({
|
|
51
|
+
projectId: 'test',
|
|
52
|
+
credentials: {
|
|
53
|
+
client_email: 'test@genkit.com',
|
|
54
|
+
private_key: '-----BEGIN PRIVATE KEY-----',
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('GoogleCloudLogs', () => {
|
|
62
|
+
let logLines = '';
|
|
63
|
+
const logStream = new Writable();
|
|
64
|
+
logStream._write = (chunk, encoding, next) => {
|
|
65
|
+
logLines = logLines += chunk.toString();
|
|
66
|
+
next();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let ai: Genkit;
|
|
70
|
+
|
|
71
|
+
beforeAll(async () => {
|
|
72
|
+
process.env.GCLOUD_PROJECT = 'test';
|
|
73
|
+
process.env.GENKIT_ENV = 'dev';
|
|
74
|
+
__useJsonFormatForTesting();
|
|
75
|
+
__addTransportStreamForTesting(logStream);
|
|
76
|
+
await enableGoogleCloudTelemetry({
|
|
77
|
+
projectId: 'test',
|
|
78
|
+
forceDevExport: false,
|
|
79
|
+
metricExportIntervalMillis: 100,
|
|
80
|
+
metricExportTimeoutMillis: 100,
|
|
81
|
+
});
|
|
82
|
+
ai = genkit({
|
|
83
|
+
// Force GCP Plugin to use in-memory metrics exporter
|
|
84
|
+
plugins: [],
|
|
85
|
+
});
|
|
86
|
+
await waitForLogsInit(ai, logLines);
|
|
87
|
+
});
|
|
88
|
+
beforeEach(async () => {
|
|
89
|
+
logLines = '';
|
|
90
|
+
__getSpanExporterForTesting().reset();
|
|
91
|
+
});
|
|
92
|
+
afterAll(async () => {
|
|
93
|
+
await ai.stopServers();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('with truncation', () => {
|
|
97
|
+
it('truncates large output logs', async () => {
|
|
98
|
+
const testModel = createModel(ai, 'testModel', async () => {
|
|
99
|
+
return {
|
|
100
|
+
message: {
|
|
101
|
+
role: 'user',
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
text: 'r'.repeat(130_000),
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
finishReason: 'stop',
|
|
109
|
+
usage: {
|
|
110
|
+
inputTokens: 10,
|
|
111
|
+
outputTokens: 14,
|
|
112
|
+
inputCharacters: 8,
|
|
113
|
+
outputCharacters: 16,
|
|
114
|
+
inputImages: 1,
|
|
115
|
+
outputImages: 3,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
const testFlow = createFlowWithInput(ai, 'testFlow', async (input) => {
|
|
120
|
+
return await ai.run('sub1', async () => {
|
|
121
|
+
return await ai.run('sub2', async () => {
|
|
122
|
+
return await ai.generate({
|
|
123
|
+
model: testModel,
|
|
124
|
+
prompt: `${input} prompt`,
|
|
125
|
+
config: {
|
|
126
|
+
temperature: 1.0,
|
|
127
|
+
topK: 3,
|
|
128
|
+
topP: 5,
|
|
129
|
+
maxOutputTokens: 7,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await testFlow('test');
|
|
137
|
+
await getExportedSpans();
|
|
138
|
+
|
|
139
|
+
const logMessages = await getLogs(1, 100, logLines);
|
|
140
|
+
const logObjects = logMessages.map((l) => JSON.parse(l as string));
|
|
141
|
+
const logObjectMessages = logObjects.map(
|
|
142
|
+
(structuredLog) => structuredLog.message
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(logObjectMessages).toContain('Output[testFlow, testFlow]');
|
|
146
|
+
|
|
147
|
+
logObjects.map((structuredLog) => {
|
|
148
|
+
if (structuredLog.message === 'Output[testFlow, testFlow]') {
|
|
149
|
+
expect(structuredLog.content.length).toBe(128_000);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('truncates large input logs', async () => {
|
|
155
|
+
const testModel = createModel(ai, 'testModel', async () => {
|
|
156
|
+
return {
|
|
157
|
+
message: {
|
|
158
|
+
role: 'user',
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
text: 'response',
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
finishReason: 'stop',
|
|
166
|
+
usage: {
|
|
167
|
+
inputTokens: 10,
|
|
168
|
+
outputTokens: 14,
|
|
169
|
+
inputCharacters: 8,
|
|
170
|
+
outputCharacters: 16,
|
|
171
|
+
inputImages: 1,
|
|
172
|
+
outputImages: 3,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
const testFlow = createFlowWithInput(ai, 'testFlow', async (input) => {
|
|
177
|
+
return await ai.run('sub1', async () => {
|
|
178
|
+
return await ai.run('sub2', async () => {
|
|
179
|
+
return await ai.generate({
|
|
180
|
+
model: testModel,
|
|
181
|
+
prompt: `${input} prompt`,
|
|
182
|
+
config: {
|
|
183
|
+
temperature: 1.0,
|
|
184
|
+
topK: 3,
|
|
185
|
+
topP: 5,
|
|
186
|
+
maxOutputTokens: 7,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await testFlow('t'.repeat(130_000));
|
|
194
|
+
await getExportedSpans();
|
|
195
|
+
|
|
196
|
+
const logMessages = await getLogs(1, 100, logLines);
|
|
197
|
+
const logObjects = logMessages.map((l) => JSON.parse(l as string));
|
|
198
|
+
const logObjectMessages = logObjects.map(
|
|
199
|
+
(structuredLog) => structuredLog.message
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(logObjectMessages).toContain('Input[testFlow, testFlow]');
|
|
203
|
+
|
|
204
|
+
logObjects.map((structuredLog) => {
|
|
205
|
+
if (structuredLog.message === 'Input[testFlow, testFlow]') {
|
|
206
|
+
expect(structuredLog.content.length).toBe(128_000);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('truncates large model names', async () => {
|
|
212
|
+
const testModel = createModel(ai, 'm'.repeat(2046), async () => {
|
|
213
|
+
return {
|
|
214
|
+
message: {
|
|
215
|
+
role: 'user',
|
|
216
|
+
content: [
|
|
217
|
+
{
|
|
218
|
+
text: 'response',
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
finishReason: 'stop',
|
|
223
|
+
usage: {
|
|
224
|
+
inputTokens: 10,
|
|
225
|
+
outputTokens: 14,
|
|
226
|
+
inputCharacters: 8,
|
|
227
|
+
outputCharacters: 16,
|
|
228
|
+
inputImages: 1,
|
|
229
|
+
outputImages: 3,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
const testFlow = createFlowWithInput(ai, 'testFlow', async (input) => {
|
|
234
|
+
return await ai.run('sub1', async () => {
|
|
235
|
+
return await ai.run('sub2', async () => {
|
|
236
|
+
return await ai.generate({
|
|
237
|
+
model: testModel,
|
|
238
|
+
prompt: `${input} prompt`,
|
|
239
|
+
config: {
|
|
240
|
+
temperature: 1.0,
|
|
241
|
+
topK: 3,
|
|
242
|
+
topP: 5,
|
|
243
|
+
maxOutputTokens: 7,
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await testFlow('test');
|
|
251
|
+
await getExportedSpans();
|
|
252
|
+
|
|
253
|
+
const logMessages = await getLogs(1, 100, logLines);
|
|
254
|
+
const logObjects = logMessages.map((l) => JSON.parse(l as string));
|
|
255
|
+
const logObjectModels = logObjects.map(
|
|
256
|
+
(structuredLog) => structuredLog.model
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
expect(logObjectModels).toContain('m'.repeat(1024));
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('path logs', () => {
|
|
264
|
+
it('writes error log for failed path', async () => {
|
|
265
|
+
const testFlow = createFlow(ai, 'testFlow', async () => {
|
|
266
|
+
await ai.run('sub1', async () => {
|
|
267
|
+
return 'not failing';
|
|
268
|
+
});
|
|
269
|
+
await ai.run('sub2', async () => {
|
|
270
|
+
return explode();
|
|
271
|
+
});
|
|
272
|
+
return 'never reached';
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await assert.rejects(async () => {
|
|
276
|
+
await testFlow();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
await getExportedSpans();
|
|
280
|
+
|
|
281
|
+
const logs = await getLogs(1, 100, logLines);
|
|
282
|
+
const logObjectMessages = getStructuredLogMessages(logs);
|
|
283
|
+
expect(logObjectMessages).toContain(
|
|
284
|
+
"Error[testFlow > sub2, TypeError] Cannot read properties of undefined (reading 'explode')"
|
|
285
|
+
);
|
|
286
|
+
const errorLogs = logObjectMessages.filter(
|
|
287
|
+
(m) => m.indexOf('Error[') >= 0
|
|
288
|
+
);
|
|
289
|
+
expect(errorLogs).toHaveLength(1); // Only 1 error log
|
|
290
|
+
}, 10000); //timeout
|
|
291
|
+
|
|
292
|
+
it('writes error log for failed root', async () => {
|
|
293
|
+
const testFlow = createFlow(ai, 'testFlow', async () => {
|
|
294
|
+
await ai.run('sub1', async () => {
|
|
295
|
+
return 'not failing';
|
|
296
|
+
});
|
|
297
|
+
await ai.run('sub2', async () => {
|
|
298
|
+
return 'not failing';
|
|
299
|
+
});
|
|
300
|
+
return explode();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await assert.rejects(async () => {
|
|
304
|
+
await testFlow();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await getExportedSpans();
|
|
308
|
+
|
|
309
|
+
const logs = await getLogs(1, 100, logLines);
|
|
310
|
+
const logObjectMessages = getStructuredLogMessages(logs);
|
|
311
|
+
expect(logObjectMessages).toContain(
|
|
312
|
+
"Error[testFlow, TypeError] Cannot read properties of undefined (reading 'explode')"
|
|
313
|
+
);
|
|
314
|
+
const errorLogs = logObjectMessages.filter(
|
|
315
|
+
(m) => m.indexOf('Error[') >= 0
|
|
316
|
+
);
|
|
317
|
+
expect(errorLogs).toHaveLength(1); // Only 1 error log
|
|
318
|
+
}, 10000); //timeout
|
|
319
|
+
|
|
320
|
+
it('writes error log for multiple failing spans', async () => {
|
|
321
|
+
const testFlow = createFlow(ai, 'testFlow', async () => {
|
|
322
|
+
await Promise.all([
|
|
323
|
+
ai.run('sub1', async () => {
|
|
324
|
+
return explode();
|
|
325
|
+
}),
|
|
326
|
+
ai.run('sub2', async () => {
|
|
327
|
+
return explode();
|
|
328
|
+
}),
|
|
329
|
+
]);
|
|
330
|
+
return 'not failing';
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await assert.rejects(async () => {
|
|
334
|
+
await testFlow();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await getExportedSpans();
|
|
338
|
+
|
|
339
|
+
const logs = await getLogs(1, 100, logLines);
|
|
340
|
+
const logObjectMessages = getStructuredLogMessages(logs);
|
|
341
|
+
expect(logObjectMessages).toContain(
|
|
342
|
+
"Error[testFlow > sub1, TypeError] Cannot read properties of undefined (reading 'explode')"
|
|
343
|
+
);
|
|
344
|
+
expect(logObjectMessages).toContain(
|
|
345
|
+
"Error[testFlow > sub2, TypeError] Cannot read properties of undefined (reading 'explode')"
|
|
346
|
+
);
|
|
347
|
+
const errorLogs = logObjectMessages.filter(
|
|
348
|
+
(m) => m.indexOf('Error[') >= 0
|
|
349
|
+
);
|
|
350
|
+
expect(errorLogs).toHaveLength(2); // Only 2 error log
|
|
351
|
+
}, 10000); //timeout
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('writes generate logs', async () => {
|
|
355
|
+
const testModel = createModel(ai, 'testModel', async () => {
|
|
356
|
+
return {
|
|
357
|
+
message: {
|
|
358
|
+
role: 'model',
|
|
359
|
+
content: [
|
|
360
|
+
{
|
|
361
|
+
text: 'response',
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
},
|
|
365
|
+
finishReason: 'stop',
|
|
366
|
+
usage: {
|
|
367
|
+
inputTokens: 10,
|
|
368
|
+
outputTokens: 14,
|
|
369
|
+
inputCharacters: 8,
|
|
370
|
+
outputCharacters: 16,
|
|
371
|
+
inputImages: 1,
|
|
372
|
+
outputImages: 3,
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
const testFlow = createFlowWithInput(ai, 'testFlow', async (input) => {
|
|
377
|
+
return await ai.run('sub1', async () => {
|
|
378
|
+
return await ai.run('sub2', async () => {
|
|
379
|
+
return await ai.generate({
|
|
380
|
+
model: testModel,
|
|
381
|
+
prompt: `${input} prompt`,
|
|
382
|
+
config: {
|
|
383
|
+
temperature: 1.0,
|
|
384
|
+
topK: 3,
|
|
385
|
+
topP: 5,
|
|
386
|
+
maxOutputTokens: 7,
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
await testFlow('test');
|
|
394
|
+
|
|
395
|
+
await getExportedSpans();
|
|
396
|
+
|
|
397
|
+
const logs = await getLogs(1, 100, logLines);
|
|
398
|
+
expect(logs.length).toEqual(9);
|
|
399
|
+
const logObjectMessages = getStructuredLogMessages(logs);
|
|
400
|
+
expect(logObjectMessages).toContain(
|
|
401
|
+
'Config[testFlow > sub1 > sub2 > generate > testModel, testModel]'
|
|
402
|
+
);
|
|
403
|
+
expect(logObjectMessages).toContain(
|
|
404
|
+
'Input[testFlow > sub1 > sub2 > generate > testModel, testModel] '
|
|
405
|
+
);
|
|
406
|
+
expect(logObjectMessages).toContain(
|
|
407
|
+
'Output[testFlow > sub1 > sub2 > generate > testModel, testModel] '
|
|
408
|
+
);
|
|
409
|
+
expect(logObjectMessages).toContain('Input[testFlow, testFlow]');
|
|
410
|
+
expect(logObjectMessages).toContain('Output[testFlow, testFlow]');
|
|
411
|
+
expect(logObjectMessages).toContain(
|
|
412
|
+
'Input[testFlow > sub1 > sub2 > generate, testFlow]'
|
|
413
|
+
);
|
|
414
|
+
expect(logObjectMessages).toContain(
|
|
415
|
+
'Output[testFlow > sub1 > sub2 > generate, testFlow]'
|
|
416
|
+
);
|
|
417
|
+
// Ensure the model input/output has an associated role
|
|
418
|
+
logs.forEach((log) => {
|
|
419
|
+
const structuredLog = JSON.parse(log as string);
|
|
420
|
+
if (
|
|
421
|
+
structuredLog.message ===
|
|
422
|
+
'Input[testFlow > sub1 > sub2 > generate > testModel, testModel] '
|
|
423
|
+
) {
|
|
424
|
+
expect(structuredLog.role).toBe('user');
|
|
425
|
+
}
|
|
426
|
+
if (
|
|
427
|
+
structuredLog.message ===
|
|
428
|
+
'Output[testFlow > sub1 > sub2 > generate > testModel, testModel]'
|
|
429
|
+
) {
|
|
430
|
+
expect(structuredLog.role).toBe('model');
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('writes feature logs for generate without flow', async () => {
|
|
436
|
+
const testModel = createModel(ai, 'testModel', async () => {
|
|
437
|
+
return {
|
|
438
|
+
message: {
|
|
439
|
+
role: 'model',
|
|
440
|
+
content: [
|
|
441
|
+
{
|
|
442
|
+
text: 'response',
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
},
|
|
446
|
+
finishReason: 'stop',
|
|
447
|
+
usage: {
|
|
448
|
+
inputTokens: 10,
|
|
449
|
+
outputTokens: 14,
|
|
450
|
+
inputCharacters: 8,
|
|
451
|
+
outputCharacters: 16,
|
|
452
|
+
inputImages: 1,
|
|
453
|
+
outputImages: 3,
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
await ai.generate({
|
|
459
|
+
model: testModel,
|
|
460
|
+
prompt: `a test prompt`,
|
|
461
|
+
config: {
|
|
462
|
+
temperature: 1.0,
|
|
463
|
+
topK: 3,
|
|
464
|
+
topP: 5,
|
|
465
|
+
maxOutputTokens: 7,
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
await getExportedSpans();
|
|
470
|
+
|
|
471
|
+
const logs = await getLogs(1, 100, logLines);
|
|
472
|
+
expect(logs.length).toEqual(6);
|
|
473
|
+
const logObjectMessages = getStructuredLogMessages(logs);
|
|
474
|
+
expect(logObjectMessages).toContain(
|
|
475
|
+
'Config[generate > testModel, testModel]'
|
|
476
|
+
);
|
|
477
|
+
expect(logObjectMessages).toContain(
|
|
478
|
+
'Input[generate > testModel, testModel] '
|
|
479
|
+
);
|
|
480
|
+
expect(logObjectMessages).toContain(
|
|
481
|
+
'Output[generate > testModel, testModel] '
|
|
482
|
+
);
|
|
483
|
+
expect(logObjectMessages).toContain('Input[generate, generate]');
|
|
484
|
+
expect(logObjectMessages).toContain('Output[generate, generate]');
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('writes user feedback log', async () => {
|
|
488
|
+
await appendSpan(
|
|
489
|
+
'trace1',
|
|
490
|
+
'parent1',
|
|
491
|
+
{
|
|
492
|
+
name: 'user-feedback',
|
|
493
|
+
path: '/{flowName}',
|
|
494
|
+
metadata: {
|
|
495
|
+
subtype: 'userFeedback',
|
|
496
|
+
feedbackValue: 'negative',
|
|
497
|
+
textFeedback: 'terrible',
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
{ [SPAN_TYPE_ATTR]: 'userEngagement' }
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
await getExportedSpans();
|
|
504
|
+
const logs = await getLogs(1, 100, logLines);
|
|
505
|
+
const logObjectMessages = getStructuredLogMessages(logs);
|
|
506
|
+
expect(logObjectMessages).toContain('UserFeedback[flowName]');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('writes user acceptance log', async () => {
|
|
510
|
+
await appendSpan(
|
|
511
|
+
'trace1',
|
|
512
|
+
'parent1',
|
|
513
|
+
{
|
|
514
|
+
name: 'user-acceptance',
|
|
515
|
+
path: '/{flowName}',
|
|
516
|
+
metadata: { subtype: 'userAcceptance', acceptanceValue: 'rejected' },
|
|
517
|
+
},
|
|
518
|
+
{ [SPAN_TYPE_ATTR]: 'userEngagement' }
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
await getExportedSpans();
|
|
522
|
+
const logs = await getLogs(1, 100, logLines);
|
|
523
|
+
const logObjectMessages = getStructuredLogMessages(logs);
|
|
524
|
+
expect(logObjectMessages).toContain('UserAcceptance[flowName]');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('writes tool input and output logs', async () => {
|
|
528
|
+
const echoTool = ai.defineTool(
|
|
529
|
+
{ name: 'echoTool', description: 'echo' },
|
|
530
|
+
async (input) => input
|
|
531
|
+
);
|
|
532
|
+
await echoTool('Helllooooo!');
|
|
533
|
+
await getExportedSpans();
|
|
534
|
+
const logs = await getLogs(1, 100, logLines);
|
|
535
|
+
const logObjectMessages = getStructuredLogMessages(logs);
|
|
536
|
+
expect(logObjectMessages).toContain('Input[echoTool, echoTool]');
|
|
537
|
+
expect(logObjectMessages).toContain('Output[echoTool, echoTool]');
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
/** Helper to create a flow with no inputs or outputs */
|
|
542
|
+
function createFlow(
|
|
543
|
+
ai: Genkit,
|
|
544
|
+
name: string,
|
|
545
|
+
fn: () => Promise<any> = async () => {}
|
|
546
|
+
) {
|
|
547
|
+
return ai.defineFlow(
|
|
548
|
+
{
|
|
549
|
+
name,
|
|
550
|
+
inputSchema: z.void(),
|
|
551
|
+
outputSchema: z.void(),
|
|
552
|
+
},
|
|
553
|
+
fn
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function createFlowWithInput(
|
|
558
|
+
ai: Genkit,
|
|
559
|
+
name: string,
|
|
560
|
+
fn: (input: string) => Promise<any>
|
|
561
|
+
) {
|
|
562
|
+
return ai.defineFlow(
|
|
563
|
+
{
|
|
564
|
+
name,
|
|
565
|
+
inputSchema: z.string(),
|
|
566
|
+
outputSchema: z.any(),
|
|
567
|
+
},
|
|
568
|
+
fn
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Helper to create a model that returns the value produced by the given
|
|
574
|
+
* response function.
|
|
575
|
+
*/
|
|
576
|
+
function createModel(
|
|
577
|
+
genkit: Genkit,
|
|
578
|
+
name: string,
|
|
579
|
+
respFn: () => Promise<GenerateResponseData>
|
|
580
|
+
) {
|
|
581
|
+
return genkit.defineModel({ name }, (req) => respFn());
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function waitForLogsInit(genkit: Genkit, logLines: any) {
|
|
585
|
+
await import('winston');
|
|
586
|
+
const testFlow = createFlow(genkit, 'testLogsInitFlow');
|
|
587
|
+
await testFlow();
|
|
588
|
+
await getLogs(1, 100, logLines);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function getLogs(
|
|
592
|
+
logCount: number,
|
|
593
|
+
maxAttempts: number,
|
|
594
|
+
logLines: string
|
|
595
|
+
): Promise<string[]> {
|
|
596
|
+
var attempts = 0;
|
|
597
|
+
while (attempts++ < maxAttempts) {
|
|
598
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
599
|
+
const found = logLines
|
|
600
|
+
.trim()
|
|
601
|
+
.split('\n')
|
|
602
|
+
.map((l) => l.trim());
|
|
603
|
+
if (found.length >= logCount) {
|
|
604
|
+
return found;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
assert.fail(`Waiting for logs, but none have been written.`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** Polls the in memory metric exporter until the genkit scope is found. */
|
|
611
|
+
async function getExportedSpans(maxAttempts = 200): Promise<ReadableSpan[]> {
|
|
612
|
+
__forceFlushSpansForTesting();
|
|
613
|
+
var attempts = 0;
|
|
614
|
+
while (attempts++ < maxAttempts) {
|
|
615
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
616
|
+
const found = __getSpanExporterForTesting().getFinishedSpans();
|
|
617
|
+
if (found.length > 0) {
|
|
618
|
+
return found;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
assert.fail(`Timed out while waiting for spans to be exported.`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function getStructuredLogMessages(logs: string[]): string[] {
|
|
625
|
+
const logObjects = logs.map((l) => JSON.parse(l as string));
|
|
626
|
+
return logObjects.map((log) => log.message);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function explode() {
|
|
630
|
+
const nothing: { missing?: any } = { missing: 1 };
|
|
631
|
+
delete nothing.missing;
|
|
632
|
+
return nothing.missing.explode;
|
|
633
|
+
}
|