@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.
Files changed (108) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +83 -0
  3. package/lib/auth.d.mts +33 -0
  4. package/lib/auth.d.ts +33 -0
  5. package/lib/auth.js +70 -0
  6. package/lib/auth.js.map +1 -0
  7. package/lib/auth.mjs +45 -0
  8. package/lib/auth.mjs.map +1 -0
  9. package/lib/gcpLogger.d.mts +25 -0
  10. package/lib/gcpLogger.d.ts +25 -0
  11. package/lib/gcpLogger.js +118 -0
  12. package/lib/gcpLogger.js.map +1 -0
  13. package/lib/gcpLogger.mjs +82 -0
  14. package/lib/gcpLogger.mjs.map +1 -0
  15. package/lib/gcpOpenTelemetry.d.mts +59 -0
  16. package/lib/gcpOpenTelemetry.d.ts +59 -0
  17. package/lib/gcpOpenTelemetry.js +374 -0
  18. package/lib/gcpOpenTelemetry.js.map +1 -0
  19. package/lib/gcpOpenTelemetry.mjs +364 -0
  20. package/lib/gcpOpenTelemetry.mjs.map +1 -0
  21. package/lib/index.d.mts +36 -0
  22. package/lib/index.d.ts +36 -0
  23. package/lib/index.js +56 -0
  24. package/lib/index.js.map +1 -0
  25. package/lib/index.mjs +29 -0
  26. package/lib/index.mjs.map +1 -0
  27. package/lib/metrics.d.mts +65 -0
  28. package/lib/metrics.d.ts +65 -0
  29. package/lib/metrics.js +91 -0
  30. package/lib/metrics.js.map +1 -0
  31. package/lib/metrics.mjs +65 -0
  32. package/lib/metrics.mjs.map +1 -0
  33. package/lib/model-armor.d.mts +59 -0
  34. package/lib/model-armor.d.ts +59 -0
  35. package/lib/model-armor.js +205 -0
  36. package/lib/model-armor.js.map +1 -0
  37. package/lib/model-armor.mjs +181 -0
  38. package/lib/model-armor.mjs.map +1 -0
  39. package/lib/telemetry/action.d.mts +27 -0
  40. package/lib/telemetry/action.d.ts +27 -0
  41. package/lib/telemetry/action.js +92 -0
  42. package/lib/telemetry/action.js.map +1 -0
  43. package/lib/telemetry/action.mjs +73 -0
  44. package/lib/telemetry/action.mjs.map +1 -0
  45. package/lib/telemetry/defaults.d.mts +30 -0
  46. package/lib/telemetry/defaults.d.ts +30 -0
  47. package/lib/telemetry/defaults.js +70 -0
  48. package/lib/telemetry/defaults.js.map +1 -0
  49. package/lib/telemetry/defaults.mjs +46 -0
  50. package/lib/telemetry/defaults.mjs.map +1 -0
  51. package/lib/telemetry/engagement.d.mts +35 -0
  52. package/lib/telemetry/engagement.d.ts +35 -0
  53. package/lib/telemetry/engagement.js +106 -0
  54. package/lib/telemetry/engagement.js.map +1 -0
  55. package/lib/telemetry/engagement.mjs +85 -0
  56. package/lib/telemetry/engagement.mjs.map +1 -0
  57. package/lib/telemetry/feature.d.mts +35 -0
  58. package/lib/telemetry/feature.d.ts +35 -0
  59. package/lib/telemetry/feature.js +142 -0
  60. package/lib/telemetry/feature.js.map +1 -0
  61. package/lib/telemetry/feature.mjs +127 -0
  62. package/lib/telemetry/feature.mjs.map +1 -0
  63. package/lib/telemetry/generate.d.mts +53 -0
  64. package/lib/telemetry/generate.d.ts +53 -0
  65. package/lib/telemetry/generate.js +326 -0
  66. package/lib/telemetry/generate.js.map +1 -0
  67. package/lib/telemetry/generate.mjs +314 -0
  68. package/lib/telemetry/generate.mjs.map +1 -0
  69. package/lib/telemetry/path.d.mts +32 -0
  70. package/lib/telemetry/path.d.ts +32 -0
  71. package/lib/telemetry/path.js +91 -0
  72. package/lib/telemetry/path.js.map +1 -0
  73. package/lib/telemetry/path.mjs +78 -0
  74. package/lib/telemetry/path.mjs.map +1 -0
  75. package/lib/types.d.mts +121 -0
  76. package/lib/types.d.ts +121 -0
  77. package/lib/types.js +17 -0
  78. package/lib/types.js.map +1 -0
  79. package/lib/types.mjs +1 -0
  80. package/lib/types.mjs.map +1 -0
  81. package/lib/utils.d.mts +57 -0
  82. package/lib/utils.d.ts +57 -0
  83. package/lib/utils.js +143 -0
  84. package/lib/utils.js.map +1 -0
  85. package/lib/utils.mjs +104 -0
  86. package/lib/utils.mjs.map +1 -0
  87. package/package.json +89 -0
  88. package/src/auth.ts +89 -0
  89. package/src/gcpLogger.ts +124 -0
  90. package/src/gcpOpenTelemetry.ts +485 -0
  91. package/src/index.ts +59 -0
  92. package/src/metrics.ts +122 -0
  93. package/src/model-armor.ts +317 -0
  94. package/src/telemetry/action.ts +106 -0
  95. package/src/telemetry/defaults.ts +72 -0
  96. package/src/telemetry/engagement.ts +120 -0
  97. package/src/telemetry/feature.ts +170 -0
  98. package/src/telemetry/generate.ts +454 -0
  99. package/src/telemetry/path.ts +111 -0
  100. package/src/types.ts +133 -0
  101. package/src/utils.ts +175 -0
  102. package/tests/logs_no_input_output_test.ts +267 -0
  103. package/tests/logs_session_test.ts +219 -0
  104. package/tests/logs_test.ts +633 -0
  105. package/tests/metrics_test.ts +792 -0
  106. package/tests/model_armor_test.ts +336 -0
  107. package/tests/traces_test.ts +380 -0
  108. package/typedoc.json +3 -0
@@ -0,0 +1,111 @@
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 } from '@opentelemetry/api';
18
+ import { hrTimeDuration, hrTimeToMilliseconds } from '@opentelemetry/core';
19
+ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
20
+ import { GENKIT_VERSION } from 'genkit';
21
+ import { logger } from 'genkit/logging';
22
+ import { toDisplayPath } from 'genkit/tracing';
23
+ import {
24
+ MetricCounter,
25
+ MetricHistogram,
26
+ internalMetricNamespaceWrap,
27
+ type Telemetry,
28
+ } from '../metrics.js';
29
+ import {
30
+ createCommonLogAttributes,
31
+ extractErrorMessage,
32
+ extractErrorName,
33
+ extractErrorStack,
34
+ extractOuterFeatureNameFromPath,
35
+ truncatePath,
36
+ } from '../utils.js';
37
+
38
+ class PathsTelemetry implements Telemetry {
39
+ /**
40
+ * Wraps the declared metrics in a Genkit-specific, internal namespace.
41
+ */
42
+ private _N = internalMetricNamespaceWrap.bind(null, 'feature');
43
+
44
+ private pathCounter = new MetricCounter(this._N('path/requests'), {
45
+ description: 'Tracks unique flow paths per flow.',
46
+ valueType: ValueType.INT,
47
+ });
48
+
49
+ private pathLatencies = new MetricHistogram(this._N('path/latency'), {
50
+ description: 'Latencies per flow path.',
51
+ ValueType: ValueType.DOUBLE,
52
+ unit: 'ms',
53
+ });
54
+
55
+ tick(
56
+ span: ReadableSpan,
57
+ logInputAndOutput: boolean,
58
+ projectId?: string
59
+ ): void {
60
+ const attributes = span.attributes;
61
+
62
+ const path = attributes['genkit:path'] as string;
63
+
64
+ const isFailureSource = !!span.attributes['genkit:isFailureSource'];
65
+ const state = attributes['genkit:state'] as string;
66
+
67
+ if (!path || !isFailureSource || state !== 'error') {
68
+ // Only tick metrics for failing, leaf spans.
69
+ return;
70
+ }
71
+
72
+ const sessionId = attributes['genkit:sessionId'] as string;
73
+ const threadName = attributes['genkit:threadName'] as string;
74
+
75
+ const errorName = extractErrorName(span.events) || '<unknown>';
76
+ const errorMessage = extractErrorMessage(span.events) || '<unknown>';
77
+ const errorStack = extractErrorStack(span.events) || '';
78
+
79
+ const latency = hrTimeToMilliseconds(
80
+ hrTimeDuration(span.startTime, span.endTime)
81
+ );
82
+
83
+ const pathDimensions = {
84
+ featureName: extractOuterFeatureNameFromPath(path),
85
+ status: 'failure',
86
+ error: errorName,
87
+ path: path,
88
+ source: 'ts',
89
+ sourceVersion: GENKIT_VERSION,
90
+ };
91
+ this.pathCounter.add(1, pathDimensions);
92
+ this.pathLatencies.record(latency, pathDimensions);
93
+
94
+ const displayPath = truncatePath(toDisplayPath(path));
95
+ logger.logStructuredError(`Error[${displayPath}, ${errorName}]`, {
96
+ ...createCommonLogAttributes(span, projectId),
97
+ path: displayPath,
98
+ qualifiedPath: path,
99
+ name: errorName,
100
+ message: errorMessage,
101
+ stack: errorStack,
102
+ source: 'ts',
103
+ sourceVersion: GENKIT_VERSION,
104
+ sessionId,
105
+ threadName,
106
+ });
107
+ }
108
+ }
109
+
110
+ const pathsTelemetry = new PathsTelemetry();
111
+ export { pathsTelemetry };
package/src/types.ts ADDED
@@ -0,0 +1,133 @@
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 { InstrumentationConfigMap } from '@opentelemetry/auto-instrumentations-node';
18
+ import type { Instrumentation } from '@opentelemetry/instrumentation';
19
+ import type { Sampler } from '@opentelemetry/sdk-trace-base';
20
+ import type { JWTInput } from 'google-auth-library';
21
+
22
+ /** Configuration options for the Google Cloud plugin. */
23
+ export interface GcpTelemetryConfigOptions {
24
+ /**
25
+ * Google Cloud Project ID. If provided, will take precedence over the
26
+ * projectId inferred from the application credential and/or environment.
27
+ * Required when providing an external credential (e.g. Workload Identity
28
+ * Federation.)
29
+ */
30
+ projectId?: string;
31
+
32
+ /**
33
+ * Credentials for authenticating with Google Cloud. Primarily intended for
34
+ * use in environments outside of GCP. On GCP credentials will typically be
35
+ * inferred from the environment via Application Default Credentials (ADC).
36
+ */
37
+ credentials?: JWTInput;
38
+
39
+ /**
40
+ * OpenTelemetry sampler; controls the number of traces collected and exported
41
+ * to Google Cloud. Defaults to AlwaysOnSampler, which will collect and export
42
+ * all traces.
43
+ *
44
+ * There are four built-in samplers to choose from:
45
+ *
46
+ * - {@link https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/sampler/AlwaysOnSampler.ts | AlwaysOnSampler} - samples all traces
47
+ * - {@link https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/sampler/AlwaysOffSampler.ts | AlwaysOffSampler} - samples no traces
48
+ * - {@link https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/sampler/ParentBasedSampler.ts | ParentBasedSampler} - samples based on parent span
49
+ * - {@link https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/sampler/TraceIdRatioBasedSampler.ts | TraceIdRatioBasedSampler} - samples a configurable percentage of traces
50
+ */
51
+ sampler?: Sampler;
52
+
53
+ /**
54
+ * Enabled by default, OpenTelemetry will automatically collect telemetry for
55
+ * popular libraries via auto instrumentations without any additional code
56
+ * or configuration. All available instrumentations will be collected, unless
57
+ * otherwise specified via {@link autoInstrumentationConfig}.
58
+ *
59
+ * @see https://opentelemetry.io/docs/zero-code/js/
60
+ */
61
+ autoInstrumentation?: boolean;
62
+
63
+ /**
64
+ * Map of auto instrumentations and their configuration options. Available
65
+ * options will vary by instrumentation.
66
+ *
67
+ * @see https://opentelemetry.io/docs/zero-code/js/
68
+ */
69
+ autoInstrumentationConfig?: InstrumentationConfigMap;
70
+
71
+ /**
72
+ * Additional OpenTelemetry instrumentations to include, beyond those
73
+ * provided by auto instrumentations.
74
+ */
75
+ instrumentations?: Instrumentation[];
76
+
77
+ /**
78
+ * Metrics export interval in milliseconds; Google Cloud requires a minimum
79
+ * value of 5000ms.
80
+ */
81
+ metricExportIntervalMillis?: number;
82
+
83
+ /**
84
+ * Timeout for the metrics export in milliseconds.
85
+ */
86
+ metricExportTimeoutMillis?: number;
87
+
88
+ /**
89
+ * If set to true, metrics will not be exported to Google Cloud. Traces and
90
+ * logs may still be exported.
91
+ */
92
+ disableMetrics?: boolean;
93
+
94
+ /**
95
+ * If set to true, traces will not be exported to Google Cloud. Metrics and
96
+ * logs may still be exported.
97
+ */
98
+ disableTraces?: boolean;
99
+
100
+ /**
101
+ * If set to true, input and output logs will not be collected.
102
+ */
103
+ disableLoggingInputAndOutput?: boolean;
104
+
105
+ /**
106
+ * If set to true, telemetry data will be exported in the Genkit `dev`
107
+ * environment. Useful for local testing and troubleshooting; default is
108
+ * false.
109
+ */
110
+ forceDevExport?: boolean;
111
+ }
112
+
113
+ /** Internal telemetry configuration. */
114
+ export interface GcpTelemetryConfig {
115
+ projectId?: string;
116
+ credentials?: JWTInput;
117
+
118
+ sampler: Sampler;
119
+ autoInstrumentation: boolean;
120
+ autoInstrumentationConfig: InstrumentationConfigMap;
121
+ metricExportIntervalMillis: number;
122
+ metricExportTimeoutMillis: number;
123
+ instrumentations: Instrumentation[];
124
+ disableMetrics: boolean;
125
+ disableTraces: boolean;
126
+ exportInputAndOutput: boolean;
127
+ export: boolean;
128
+ }
129
+
130
+ export interface GcpPrincipal {
131
+ projectId?: string;
132
+ serviceAccountEmail?: string;
133
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,175 @@
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 { TraceFlags } from '@opentelemetry/api';
18
+ import type { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base';
19
+ import { resolveCurrentPrincipal } from './auth.js';
20
+
21
+ /**
22
+ * The maximum length (in characters) of a logged input or output.
23
+ * This limit exists to align the logs with GCP logging size limits.
24
+ * */
25
+ const MAX_LOG_CONTENT_CHARS = 128_000;
26
+
27
+ /**
28
+ * The maximum length (in characters) of a flow path.
29
+ */
30
+ const MAX_PATH_CHARS = 4096;
31
+
32
+ export function extractOuterFlowNameFromPath(path: string) {
33
+ if (!path || path === '<unknown>') {
34
+ return '<unknown>';
35
+ }
36
+
37
+ const flowName = path.match('/{(.+),t:flow}+');
38
+ return flowName ? flowName[1] : '<unknown>';
39
+ }
40
+
41
+ export function truncate(
42
+ text: string,
43
+ limit: number = MAX_LOG_CONTENT_CHARS
44
+ ): string {
45
+ return text ? text.substring(0, limit) : text;
46
+ }
47
+
48
+ export function truncatePath(path: string) {
49
+ return truncate(path, MAX_PATH_CHARS);
50
+ }
51
+
52
+ /**
53
+ * Extract first feature name from a path
54
+ * e.g. for /{myFlow,t:flow}/{myStep,t:flowStep}/{googleai/gemini-pro,t:action,s:model}
55
+ * returns "myFlow"
56
+ */
57
+ export function extractOuterFeatureNameFromPath(path: string) {
58
+ if (!path || path === '<unknown>') {
59
+ return '<unknown>';
60
+ }
61
+ const first = path.split('/')[1];
62
+ const featureName = first?.match(
63
+ '{(.+),t:(flow|action|prompt|dotprompt|helper)'
64
+ );
65
+ return featureName ? featureName[1] : '<unknown>';
66
+ }
67
+
68
+ export function extractErrorName(events: TimedEvent[]): string | undefined {
69
+ return events
70
+ .filter((event) => event.name === 'exception')
71
+ .map((event) => {
72
+ const attributes = event.attributes;
73
+ return attributes
74
+ ? truncate(attributes['exception.type'] as string, 1024)
75
+ : '<unknown>';
76
+ })
77
+ .at(0);
78
+ }
79
+
80
+ export function extractErrorMessage(events: TimedEvent[]): string | undefined {
81
+ return events
82
+ .filter((event) => event.name === 'exception')
83
+ .map((event) => {
84
+ const attributes = event.attributes;
85
+ return attributes
86
+ ? truncate(attributes['exception.message'] as string, 4096)
87
+ : '<unknown>';
88
+ })
89
+ .at(0);
90
+ }
91
+
92
+ export function extractErrorStack(events: TimedEvent[]): string | undefined {
93
+ return events
94
+ .filter((event) => event.name === 'exception')
95
+ .map((event) => {
96
+ const attributes = event.attributes;
97
+ return attributes
98
+ ? truncate(attributes['exception.stacktrace'] as string, 32_768)
99
+ : '<unknown>';
100
+ })
101
+ .at(0);
102
+ }
103
+
104
+ export function createCommonLogAttributes(
105
+ span: ReadableSpan,
106
+ projectId?: string
107
+ ) {
108
+ const spanContext = span.spanContext();
109
+ const isSampled = !!(spanContext.traceFlags & TraceFlags.SAMPLED);
110
+ return {
111
+ 'logging.googleapis.com/spanId': spanContext.spanId,
112
+ 'logging.googleapis.com/trace': `projects/${projectId}/traces/${spanContext.traceId}`,
113
+ 'logging.googleapis.com/trace_sampled': isSampled ? '1' : '0',
114
+ };
115
+ }
116
+
117
+ export function requestDenied(
118
+ err: Error & {
119
+ code?: number;
120
+ statusDetails?: Record<string, any>[];
121
+ }
122
+ ) {
123
+ return err.code === 7;
124
+ }
125
+
126
+ export function loggingDenied(
127
+ err: Error & {
128
+ code?: number;
129
+ statusDetails?: Record<string, any>[];
130
+ }
131
+ ) {
132
+ return (
133
+ requestDenied(err) &&
134
+ err.statusDetails?.some((details) => {
135
+ return details?.metadata?.permission === 'logging.logEntries.create';
136
+ })
137
+ );
138
+ }
139
+
140
+ export function tracingDenied(
141
+ err: Error & {
142
+ code?: number;
143
+ statusDetails?: Record<string, any>[];
144
+ }
145
+ ) {
146
+ // Looks like we don't get status details like we do with logging
147
+ return requestDenied(err);
148
+ }
149
+
150
+ export function metricsDenied(
151
+ err: Error & {
152
+ code?: number;
153
+ statusDetails?: Record<string, any>[];
154
+ }
155
+ ) {
156
+ // Looks like we don't get status details like we do with logging
157
+ return requestDenied(err);
158
+ }
159
+
160
+ export async function permissionDeniedHelpText(role: string) {
161
+ const principal = await resolveCurrentPrincipal();
162
+ return `Add the role '${role}' to your Service Account in the IAM & Admin page on the Google Cloud console, or use the following command:\n\ngcloud projects add-iam-policy-binding ${principal.projectId ?? '${PROJECT_ID}'} \\\n --member=serviceAccount:${principal.serviceAccountEmail || '${SERVICE_ACCT}'} \\\n --role=${role}`;
163
+ }
164
+
165
+ export async function loggingDeniedHelpText() {
166
+ return permissionDeniedHelpText('roles/logging.logWriter');
167
+ }
168
+
169
+ export async function tracingDeniedHelpText() {
170
+ return permissionDeniedHelpText('roles/cloudtrace.agent');
171
+ }
172
+
173
+ export async function metricsDeniedHelpText() {
174
+ return permissionDeniedHelpText('roles/monitoring.metricWriter');
175
+ }
@@ -0,0 +1,267 @@
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
+ it,
23
+ jest,
24
+ } from '@jest/globals';
25
+ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
26
+ import * as assert from 'assert';
27
+ import { genkit, z, type GenerateResponseData, type Genkit } from 'genkit';
28
+ import { Writable } from 'stream';
29
+ import {
30
+ __addTransportStreamForTesting,
31
+ __forceFlushSpansForTesting,
32
+ __getSpanExporterForTesting,
33
+ enableGoogleCloudTelemetry,
34
+ } from '../src/index.js';
35
+
36
+ jest.mock('../src/auth.js', () => {
37
+ const original = jest.requireActual('../src/auth.js');
38
+ return {
39
+ ...(original || {}),
40
+ resolveCurrentPrincipal: jest.fn().mockImplementation(() => {
41
+ return Promise.resolve({
42
+ projectId: 'test',
43
+ serviceAccountEmail: 'test@test.com',
44
+ });
45
+ }),
46
+ credentialsFromEnvironment: jest.fn().mockImplementation(() => {
47
+ return Promise.resolve({
48
+ projectId: 'test',
49
+ credentials: {
50
+ client_email: 'test@genkit.com',
51
+ private_key: '-----BEGIN PRIVATE KEY-----',
52
+ },
53
+ });
54
+ }),
55
+ };
56
+ });
57
+
58
+ describe('GoogleCloudLogs no I/O', () => {
59
+ let logLines = '';
60
+ const logStream = new Writable();
61
+ logStream._write = (chunk, encoding, next) => {
62
+ logLines = logLines += chunk.toString();
63
+ next();
64
+ };
65
+
66
+ let ai: Genkit;
67
+
68
+ beforeAll(async () => {
69
+ process.env.GCLOUD_PROJECT = 'test';
70
+ process.env.GENKIT_ENV = 'dev';
71
+ __addTransportStreamForTesting(logStream);
72
+ await enableGoogleCloudTelemetry({
73
+ projectId: 'test',
74
+ forceDevExport: false,
75
+ metricExportIntervalMillis: 100,
76
+ metricExportTimeoutMillis: 100,
77
+ disableLoggingInputAndOutput: true,
78
+ });
79
+ ai = genkit({});
80
+ // Wait for the telemetry plugin to be initialized
81
+ await waitForLogsInit(ai, logLines);
82
+ });
83
+ beforeEach(async () => {
84
+ logLines = '';
85
+ __getSpanExporterForTesting().reset();
86
+ });
87
+ afterAll(async () => {
88
+ await ai.stopServers();
89
+ });
90
+
91
+ it('writes error logs', async () => {
92
+ const testFlow = createFlow(ai, 'testFlow', async () => {
93
+ const nothing: { missing?: any } = { missing: 1 };
94
+ delete nothing.missing;
95
+ return nothing.missing.explode;
96
+ });
97
+
98
+ await assert.rejects(async () => {
99
+ await testFlow();
100
+ });
101
+
102
+ await getExportedSpans();
103
+
104
+ const logMessages = await getLogs(1, 100, logLines);
105
+ assert.equal(
106
+ logMessages.includes(
107
+ "[error] Error[testFlow, TypeError] Cannot read properties of undefined (reading 'explode')"
108
+ ),
109
+ true
110
+ );
111
+ }, 10000); //timeout
112
+
113
+ it('writes generate logs', async () => {
114
+ const testModel = createModel(ai, 'testModel', async () => {
115
+ return {
116
+ message: {
117
+ role: 'user',
118
+ content: [
119
+ {
120
+ text: 'response',
121
+ },
122
+ ],
123
+ },
124
+ finishReason: 'stop',
125
+ usage: {
126
+ inputTokens: 10,
127
+ outputTokens: 14,
128
+ inputCharacters: 8,
129
+ outputCharacters: 16,
130
+ inputImages: 1,
131
+ outputImages: 3,
132
+ },
133
+ };
134
+ });
135
+ const testFlow = createFlowWithInput(ai, 'testFlow', async (input) => {
136
+ return await ai.run('sub1', async () => {
137
+ return await ai.run('sub2', async () => {
138
+ return await ai.generate({
139
+ model: testModel,
140
+ prompt: `${input} prompt`,
141
+ config: {
142
+ temperature: 1.0,
143
+ topK: 3,
144
+ topP: 5,
145
+ maxOutputTokens: 7,
146
+ },
147
+ });
148
+ });
149
+ });
150
+ });
151
+
152
+ await testFlow('test');
153
+
154
+ await getExportedSpans();
155
+
156
+ const logMessages = await getLogs(1, 100, logLines);
157
+ assert.equal(
158
+ logMessages.includes(
159
+ '[info] Config[testFlow > sub1 > sub2 > generate > testModel, testModel]'
160
+ ),
161
+ true
162
+ );
163
+ assert.equal(
164
+ logMessages.includes(
165
+ '[info] Input[testFlow > sub1 > sub2 > generate > testModel, testModel]'
166
+ ),
167
+ false
168
+ );
169
+ assert.equal(
170
+ logMessages.includes(
171
+ '[info] Output[testFlow > sub1 > sub2 > generate > testModel, testModel]'
172
+ ),
173
+ false
174
+ );
175
+ assert.equal(
176
+ logMessages.includes('[info] Input[testFlow, testModel]'),
177
+ false
178
+ );
179
+ assert.equal(
180
+ logMessages.includes('[info] Output[testFlow, testModel]'),
181
+ false
182
+ );
183
+ });
184
+ });
185
+
186
+ /** Helper to create a flow with no inputs or outputs */
187
+ function createFlow(
188
+ ai: Genkit,
189
+ name: string,
190
+ fn: () => Promise<any> = async () => {}
191
+ ) {
192
+ return ai.defineFlow(
193
+ {
194
+ name,
195
+ inputSchema: z.void(),
196
+ outputSchema: z.void(),
197
+ },
198
+ fn
199
+ );
200
+ }
201
+
202
+ function createFlowWithInput(
203
+ ai: Genkit,
204
+ name: string,
205
+ fn: (input: string) => Promise<any>
206
+ ) {
207
+ return ai.defineFlow(
208
+ {
209
+ name,
210
+ inputSchema: z.string(),
211
+ outputSchema: z.any(),
212
+ },
213
+ fn
214
+ );
215
+ }
216
+
217
+ /**
218
+ * Helper to create a model that returns the value produced by the given
219
+ * response function.
220
+ */
221
+ function createModel(
222
+ genkit: Genkit,
223
+ name: string,
224
+ respFn: () => Promise<GenerateResponseData>
225
+ ) {
226
+ return genkit.defineModel({ name }, (req) => respFn());
227
+ }
228
+
229
+ async function waitForLogsInit(genkit: Genkit, logLines: any) {
230
+ await import('winston');
231
+ const testFlow = createFlow(genkit, 'testFlow');
232
+ await testFlow();
233
+ await getLogs(1, 100, logLines);
234
+ }
235
+
236
+ async function getLogs(
237
+ logCount: number,
238
+ maxAttempts: number,
239
+ logLines: string
240
+ ): Promise<string[]> {
241
+ var attempts = 0;
242
+ while (attempts++ < maxAttempts) {
243
+ await new Promise((resolve) => setTimeout(resolve, 100));
244
+ const found = logLines
245
+ .trim()
246
+ .split('\n')
247
+ .map((l) => l.trim());
248
+ if (found.length >= logCount) {
249
+ return found;
250
+ }
251
+ }
252
+ assert.fail(`Waiting for logs, but none have been written.`);
253
+ }
254
+
255
+ /** Polls the in memory metric exporter until the genkit scope is found. */
256
+ async function getExportedSpans(maxAttempts = 200): Promise<ReadableSpan[]> {
257
+ __forceFlushSpansForTesting();
258
+ var attempts = 0;
259
+ while (attempts++ < maxAttempts) {
260
+ await new Promise((resolve) => setTimeout(resolve, 50));
261
+ const found = __getSpanExporterForTesting().getFinishedSpans();
262
+ if (found.length > 0) {
263
+ return found;
264
+ }
265
+ }
266
+ assert.fail(`Timed out while waiting for spans to be exported.`);
267
+ }