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.
Files changed (109) hide show
  1. package/package/LICENSE +203 -0
  2. package/package/README.md +83 -0
  3. package/package/lib/auth.d.mts +33 -0
  4. package/package/lib/auth.d.ts +33 -0
  5. package/package/lib/auth.js +70 -0
  6. package/package/lib/auth.js.map +1 -0
  7. package/package/lib/auth.mjs +45 -0
  8. package/package/lib/auth.mjs.map +1 -0
  9. package/package/lib/gcpLogger.d.mts +25 -0
  10. package/package/lib/gcpLogger.d.ts +25 -0
  11. package/package/lib/gcpLogger.js +118 -0
  12. package/package/lib/gcpLogger.js.map +1 -0
  13. package/package/lib/gcpLogger.mjs +82 -0
  14. package/package/lib/gcpLogger.mjs.map +1 -0
  15. package/package/lib/gcpOpenTelemetry.d.mts +59 -0
  16. package/package/lib/gcpOpenTelemetry.d.ts +59 -0
  17. package/package/lib/gcpOpenTelemetry.js +374 -0
  18. package/package/lib/gcpOpenTelemetry.js.map +1 -0
  19. package/package/lib/gcpOpenTelemetry.mjs +364 -0
  20. package/package/lib/gcpOpenTelemetry.mjs.map +1 -0
  21. package/package/lib/index.d.mts +36 -0
  22. package/package/lib/index.d.ts +36 -0
  23. package/package/lib/index.js +56 -0
  24. package/package/lib/index.js.map +1 -0
  25. package/package/lib/index.mjs +29 -0
  26. package/package/lib/index.mjs.map +1 -0
  27. package/package/lib/metrics.d.mts +65 -0
  28. package/package/lib/metrics.d.ts +65 -0
  29. package/package/lib/metrics.js +91 -0
  30. package/package/lib/metrics.js.map +1 -0
  31. package/package/lib/metrics.mjs +65 -0
  32. package/package/lib/metrics.mjs.map +1 -0
  33. package/package/lib/model-armor.d.mts +59 -0
  34. package/package/lib/model-armor.d.ts +59 -0
  35. package/package/lib/model-armor.js +205 -0
  36. package/package/lib/model-armor.js.map +1 -0
  37. package/package/lib/model-armor.mjs +181 -0
  38. package/package/lib/model-armor.mjs.map +1 -0
  39. package/package/lib/telemetry/action.d.mts +27 -0
  40. package/package/lib/telemetry/action.d.ts +27 -0
  41. package/package/lib/telemetry/action.js +92 -0
  42. package/package/lib/telemetry/action.js.map +1 -0
  43. package/package/lib/telemetry/action.mjs +73 -0
  44. package/package/lib/telemetry/action.mjs.map +1 -0
  45. package/package/lib/telemetry/defaults.d.mts +30 -0
  46. package/package/lib/telemetry/defaults.d.ts +30 -0
  47. package/package/lib/telemetry/defaults.js +70 -0
  48. package/package/lib/telemetry/defaults.js.map +1 -0
  49. package/package/lib/telemetry/defaults.mjs +46 -0
  50. package/package/lib/telemetry/defaults.mjs.map +1 -0
  51. package/package/lib/telemetry/engagement.d.mts +35 -0
  52. package/package/lib/telemetry/engagement.d.ts +35 -0
  53. package/package/lib/telemetry/engagement.js +106 -0
  54. package/package/lib/telemetry/engagement.js.map +1 -0
  55. package/package/lib/telemetry/engagement.mjs +85 -0
  56. package/package/lib/telemetry/engagement.mjs.map +1 -0
  57. package/package/lib/telemetry/feature.d.mts +35 -0
  58. package/package/lib/telemetry/feature.d.ts +35 -0
  59. package/package/lib/telemetry/feature.js +142 -0
  60. package/package/lib/telemetry/feature.js.map +1 -0
  61. package/package/lib/telemetry/feature.mjs +127 -0
  62. package/package/lib/telemetry/feature.mjs.map +1 -0
  63. package/package/lib/telemetry/generate.d.mts +53 -0
  64. package/package/lib/telemetry/generate.d.ts +53 -0
  65. package/package/lib/telemetry/generate.js +326 -0
  66. package/package/lib/telemetry/generate.js.map +1 -0
  67. package/package/lib/telemetry/generate.mjs +314 -0
  68. package/package/lib/telemetry/generate.mjs.map +1 -0
  69. package/package/lib/telemetry/path.d.mts +32 -0
  70. package/package/lib/telemetry/path.d.ts +32 -0
  71. package/package/lib/telemetry/path.js +91 -0
  72. package/package/lib/telemetry/path.js.map +1 -0
  73. package/package/lib/telemetry/path.mjs +78 -0
  74. package/package/lib/telemetry/path.mjs.map +1 -0
  75. package/package/lib/types.d.mts +121 -0
  76. package/package/lib/types.d.ts +121 -0
  77. package/package/lib/types.js +17 -0
  78. package/package/lib/types.js.map +1 -0
  79. package/package/lib/types.mjs +1 -0
  80. package/package/lib/types.mjs.map +1 -0
  81. package/package/lib/utils.d.mts +57 -0
  82. package/package/lib/utils.d.ts +57 -0
  83. package/package/lib/utils.js +143 -0
  84. package/package/lib/utils.js.map +1 -0
  85. package/package/lib/utils.mjs +104 -0
  86. package/package/lib/utils.mjs.map +1 -0
  87. package/package/package.json +90 -0
  88. package/package/src/auth.ts +89 -0
  89. package/package/src/gcpLogger.ts +124 -0
  90. package/package/src/gcpOpenTelemetry.ts +485 -0
  91. package/package/src/index.ts +59 -0
  92. package/package/src/metrics.ts +122 -0
  93. package/package/src/model-armor.ts +317 -0
  94. package/package/src/telemetry/action.ts +106 -0
  95. package/package/src/telemetry/defaults.ts +72 -0
  96. package/package/src/telemetry/engagement.ts +120 -0
  97. package/package/src/telemetry/feature.ts +170 -0
  98. package/package/src/telemetry/generate.ts +454 -0
  99. package/package/src/telemetry/path.ts +111 -0
  100. package/package/src/types.ts +133 -0
  101. package/package/src/utils.ts +175 -0
  102. package/package/tests/logs_no_input_output_test.ts +267 -0
  103. package/package/tests/logs_session_test.ts +219 -0
  104. package/package/tests/logs_test.ts +633 -0
  105. package/package/tests/metrics_test.ts +792 -0
  106. package/package/tests/model_armor_test.ts +336 -0
  107. package/package/tests/traces_test.ts +380 -0
  108. package/package/typedoc.json +3 -0
  109. 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
+ }