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,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 };
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|