@frengki0707/google-cloud-clone 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +203 -0
- package/README.md +83 -0
- package/lib/auth.d.mts +33 -0
- package/lib/auth.d.ts +33 -0
- package/lib/auth.js +70 -0
- package/lib/auth.js.map +1 -0
- package/lib/auth.mjs +45 -0
- package/lib/auth.mjs.map +1 -0
- package/lib/gcpLogger.d.mts +25 -0
- package/lib/gcpLogger.d.ts +25 -0
- package/lib/gcpLogger.js +118 -0
- package/lib/gcpLogger.js.map +1 -0
- package/lib/gcpLogger.mjs +82 -0
- package/lib/gcpLogger.mjs.map +1 -0
- package/lib/gcpOpenTelemetry.d.mts +59 -0
- package/lib/gcpOpenTelemetry.d.ts +59 -0
- package/lib/gcpOpenTelemetry.js +374 -0
- package/lib/gcpOpenTelemetry.js.map +1 -0
- package/lib/gcpOpenTelemetry.mjs +364 -0
- package/lib/gcpOpenTelemetry.mjs.map +1 -0
- package/lib/index.d.mts +36 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.js +56 -0
- package/lib/index.js.map +1 -0
- package/lib/index.mjs +29 -0
- package/lib/index.mjs.map +1 -0
- package/lib/metrics.d.mts +65 -0
- package/lib/metrics.d.ts +65 -0
- package/lib/metrics.js +91 -0
- package/lib/metrics.js.map +1 -0
- package/lib/metrics.mjs +65 -0
- package/lib/metrics.mjs.map +1 -0
- package/lib/model-armor.d.mts +59 -0
- package/lib/model-armor.d.ts +59 -0
- package/lib/model-armor.js +205 -0
- package/lib/model-armor.js.map +1 -0
- package/lib/model-armor.mjs +181 -0
- package/lib/model-armor.mjs.map +1 -0
- package/lib/telemetry/action.d.mts +27 -0
- package/lib/telemetry/action.d.ts +27 -0
- package/lib/telemetry/action.js +92 -0
- package/lib/telemetry/action.js.map +1 -0
- package/lib/telemetry/action.mjs +73 -0
- package/lib/telemetry/action.mjs.map +1 -0
- package/lib/telemetry/defaults.d.mts +30 -0
- package/lib/telemetry/defaults.d.ts +30 -0
- package/lib/telemetry/defaults.js +70 -0
- package/lib/telemetry/defaults.js.map +1 -0
- package/lib/telemetry/defaults.mjs +46 -0
- package/lib/telemetry/defaults.mjs.map +1 -0
- package/lib/telemetry/engagement.d.mts +35 -0
- package/lib/telemetry/engagement.d.ts +35 -0
- package/lib/telemetry/engagement.js +106 -0
- package/lib/telemetry/engagement.js.map +1 -0
- package/lib/telemetry/engagement.mjs +85 -0
- package/lib/telemetry/engagement.mjs.map +1 -0
- package/lib/telemetry/feature.d.mts +35 -0
- package/lib/telemetry/feature.d.ts +35 -0
- package/lib/telemetry/feature.js +142 -0
- package/lib/telemetry/feature.js.map +1 -0
- package/lib/telemetry/feature.mjs +127 -0
- package/lib/telemetry/feature.mjs.map +1 -0
- package/lib/telemetry/generate.d.mts +53 -0
- package/lib/telemetry/generate.d.ts +53 -0
- package/lib/telemetry/generate.js +326 -0
- package/lib/telemetry/generate.js.map +1 -0
- package/lib/telemetry/generate.mjs +314 -0
- package/lib/telemetry/generate.mjs.map +1 -0
- package/lib/telemetry/path.d.mts +32 -0
- package/lib/telemetry/path.d.ts +32 -0
- package/lib/telemetry/path.js +91 -0
- package/lib/telemetry/path.js.map +1 -0
- package/lib/telemetry/path.mjs +78 -0
- package/lib/telemetry/path.mjs.map +1 -0
- package/lib/types.d.mts +121 -0
- package/lib/types.d.ts +121 -0
- package/lib/types.js +17 -0
- package/lib/types.js.map +1 -0
- package/lib/types.mjs +1 -0
- package/lib/types.mjs.map +1 -0
- package/lib/utils.d.mts +57 -0
- package/lib/utils.d.ts +57 -0
- package/lib/utils.js +143 -0
- package/lib/utils.js.map +1 -0
- package/lib/utils.mjs +104 -0
- package/lib/utils.mjs.map +1 -0
- package/package.json +89 -0
- package/src/auth.ts +89 -0
- package/src/gcpLogger.ts +124 -0
- package/src/gcpOpenTelemetry.ts +485 -0
- package/src/index.ts +59 -0
- package/src/metrics.ts +122 -0
- package/src/model-armor.ts +317 -0
- package/src/telemetry/action.ts +106 -0
- package/src/telemetry/defaults.ts +72 -0
- package/src/telemetry/engagement.ts +120 -0
- package/src/telemetry/feature.ts +170 -0
- package/src/telemetry/generate.ts +454 -0
- package/src/telemetry/path.ts +111 -0
- package/src/types.ts +133 -0
- package/src/utils.ts +175 -0
- package/tests/logs_no_input_output_test.ts +267 -0
- package/tests/logs_session_test.ts +219 -0
- package/tests/logs_test.ts +633 -0
- package/tests/metrics_test.ts +792 -0
- package/tests/model_armor_test.ts +336 -0
- package/tests/traces_test.ts +380 -0
- package/typedoc.json +3 -0
|
@@ -0,0 +1,485 @@
|
|
|
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
|
+
MetricExporter,
|
|
19
|
+
type ExporterOptions,
|
|
20
|
+
} from '@google-cloud/opentelemetry-cloud-monitoring-exporter';
|
|
21
|
+
import { TraceExporter } from '@google-cloud/opentelemetry-cloud-trace-exporter';
|
|
22
|
+
import { GcpDetectorSync } from '@google-cloud/opentelemetry-resource-util';
|
|
23
|
+
import { SpanStatusCode, TraceFlags, type Span } from '@opentelemetry/api';
|
|
24
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
|
|
25
|
+
import { type ExportResult } from '@opentelemetry/core';
|
|
26
|
+
import type { Instrumentation } from '@opentelemetry/instrumentation';
|
|
27
|
+
import { PinoInstrumentation } from '@opentelemetry/instrumentation-pino';
|
|
28
|
+
import { WinstonInstrumentation } from '@opentelemetry/instrumentation-winston';
|
|
29
|
+
import { Resource } from '@opentelemetry/resources';
|
|
30
|
+
import {
|
|
31
|
+
AggregationTemporality,
|
|
32
|
+
DefaultAggregation,
|
|
33
|
+
ExponentialHistogramAggregation,
|
|
34
|
+
InMemoryMetricExporter,
|
|
35
|
+
InstrumentType,
|
|
36
|
+
PeriodicExportingMetricReader,
|
|
37
|
+
type PushMetricExporter,
|
|
38
|
+
type ResourceMetrics,
|
|
39
|
+
} from '@opentelemetry/sdk-metrics';
|
|
40
|
+
import type { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
|
|
41
|
+
import {
|
|
42
|
+
BatchSpanProcessor,
|
|
43
|
+
InMemorySpanExporter,
|
|
44
|
+
type ReadableSpan,
|
|
45
|
+
type SpanExporter,
|
|
46
|
+
} from '@opentelemetry/sdk-trace-base';
|
|
47
|
+
import { GENKIT_VERSION } from 'genkit';
|
|
48
|
+
import { logger } from 'genkit/logging';
|
|
49
|
+
import { actionTelemetry } from './telemetry/action.js';
|
|
50
|
+
import { engagementTelemetry } from './telemetry/engagement.js';
|
|
51
|
+
import { featuresTelemetry } from './telemetry/feature.js';
|
|
52
|
+
import { generateTelemetry } from './telemetry/generate.js';
|
|
53
|
+
import { pathsTelemetry } from './telemetry/path.js';
|
|
54
|
+
import type { GcpTelemetryConfig } from './types.js';
|
|
55
|
+
import {
|
|
56
|
+
metricsDenied,
|
|
57
|
+
metricsDeniedHelpText,
|
|
58
|
+
tracingDenied,
|
|
59
|
+
tracingDeniedHelpText,
|
|
60
|
+
} from './utils.js';
|
|
61
|
+
|
|
62
|
+
let metricExporter: PushMetricExporter;
|
|
63
|
+
let spanProcessor: BatchSpanProcessor;
|
|
64
|
+
let spanExporter: AdjustingTraceExporter;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Provides a {TelemetryConfig} for exporting OpenTelemetry data (Traces,
|
|
68
|
+
* Metrics, and Logs) to the Google Cloud Operations Suite.
|
|
69
|
+
*/
|
|
70
|
+
export class GcpOpenTelemetry {
|
|
71
|
+
private readonly config: GcpTelemetryConfig;
|
|
72
|
+
private readonly resource: Resource;
|
|
73
|
+
|
|
74
|
+
constructor(config: GcpTelemetryConfig) {
|
|
75
|
+
this.config = config;
|
|
76
|
+
this.resource = new Resource({ type: 'global' }).merge(
|
|
77
|
+
new GcpDetectorSync().detect()
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Log hook for writing trace and span metadata to log messages in the format
|
|
83
|
+
* required by GCP.
|
|
84
|
+
*/
|
|
85
|
+
private gcpTraceLogHook = (span: Span, record: any) => {
|
|
86
|
+
const spanContext = span.spanContext();
|
|
87
|
+
const isSampled = !!(spanContext.traceFlags & TraceFlags.SAMPLED);
|
|
88
|
+
const projectId = this.config.projectId;
|
|
89
|
+
|
|
90
|
+
record['logging.googleapis.com/trace'] ??=
|
|
91
|
+
`projects/${projectId}/traces/${spanContext.traceId}`;
|
|
92
|
+
record['logging.googleapis.com/trace_sampled'] ??= isSampled ? '1' : '0';
|
|
93
|
+
record['logging.googleapis.com/spanId'] ??= spanContext.spanId;
|
|
94
|
+
|
|
95
|
+
// Clear out the duplicate trace and span information in the log metadata.
|
|
96
|
+
// These will be incorrect for logs written during span export time since
|
|
97
|
+
// the logs are written after the span has fully executed. Those logs are
|
|
98
|
+
// explicitly tied to the correct span in createCommonLogAttributes in
|
|
99
|
+
// utils.ts.
|
|
100
|
+
delete record['span_id'];
|
|
101
|
+
delete record['trace_id'];
|
|
102
|
+
delete record['trace_flags'];
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
async getConfig(): Promise<Partial<NodeSDKConfiguration>> {
|
|
106
|
+
spanProcessor = new BatchSpanProcessor(await this.createSpanExporter());
|
|
107
|
+
return {
|
|
108
|
+
resource: this.resource,
|
|
109
|
+
spanProcessor: spanProcessor,
|
|
110
|
+
sampler: this.config.sampler,
|
|
111
|
+
instrumentations: this.getInstrumentations(),
|
|
112
|
+
metricReader: await this.createMetricReader(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async createSpanExporter(): Promise<SpanExporter> {
|
|
117
|
+
spanExporter = new AdjustingTraceExporter(
|
|
118
|
+
this.shouldExportTraces()
|
|
119
|
+
? new TraceExporter({
|
|
120
|
+
// provided projectId should take precedence over env vars, etc
|
|
121
|
+
projectId: this.config.projectId,
|
|
122
|
+
// creds for non-GCP environments, in lieu of using ADC.
|
|
123
|
+
credentials: this.config.credentials,
|
|
124
|
+
})
|
|
125
|
+
: new InMemorySpanExporter(),
|
|
126
|
+
this.config.exportInputAndOutput,
|
|
127
|
+
this.config.projectId,
|
|
128
|
+
getErrorHandler(
|
|
129
|
+
(err) => {
|
|
130
|
+
return tracingDenied(err);
|
|
131
|
+
},
|
|
132
|
+
await tracingDeniedHelpText()
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
return spanExporter;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Creates a {MetricReader} for pushing metrics out to GCP via OpenTelemetry.
|
|
140
|
+
*/
|
|
141
|
+
private async createMetricReader(): Promise<PeriodicExportingMetricReader> {
|
|
142
|
+
metricExporter = await this.buildMetricExporter();
|
|
143
|
+
return new PeriodicExportingMetricReader({
|
|
144
|
+
exportIntervalMillis: this.config.metricExportIntervalMillis,
|
|
145
|
+
exportTimeoutMillis: this.config.metricExportTimeoutMillis,
|
|
146
|
+
exporter: metricExporter,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Gets all open telemetry instrumentations as configured by the plugin. */
|
|
151
|
+
private getInstrumentations() {
|
|
152
|
+
let instrumentations: Instrumentation[] = [];
|
|
153
|
+
|
|
154
|
+
if (this.config.autoInstrumentation) {
|
|
155
|
+
instrumentations = getNodeAutoInstrumentations(
|
|
156
|
+
this.config.autoInstrumentationConfig
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return instrumentations
|
|
161
|
+
.concat(this.getDefaultLoggingInstrumentations())
|
|
162
|
+
.concat(this.config.instrumentations ?? []);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private shouldExportTraces(): boolean {
|
|
166
|
+
return this.config.export && !this.config.disableTraces;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private shouldExportMetrics(): boolean {
|
|
170
|
+
return this.config.export && !this.config.disableMetrics;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Always configure the Pino and Winston instrumentations */
|
|
174
|
+
private getDefaultLoggingInstrumentations(): Instrumentation[] {
|
|
175
|
+
return [
|
|
176
|
+
new WinstonInstrumentation({ logHook: this.gcpTraceLogHook }),
|
|
177
|
+
new PinoInstrumentation({ logHook: this.gcpTraceLogHook }),
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private async buildMetricExporter(): Promise<PushMetricExporter> {
|
|
182
|
+
const exporter: PushMetricExporter = this.shouldExportMetrics()
|
|
183
|
+
? new MetricExporterWrapper(
|
|
184
|
+
{
|
|
185
|
+
userAgent: {
|
|
186
|
+
product: 'genkit',
|
|
187
|
+
version: GENKIT_VERSION,
|
|
188
|
+
},
|
|
189
|
+
// provided projectId should take precedence over env vars, etc
|
|
190
|
+
projectId: this.config.projectId,
|
|
191
|
+
// creds for non-GCP environments, in lieu of using ADC.
|
|
192
|
+
credentials: this.config.credentials,
|
|
193
|
+
},
|
|
194
|
+
getErrorHandler(
|
|
195
|
+
(err) => {
|
|
196
|
+
return metricsDenied(err);
|
|
197
|
+
},
|
|
198
|
+
await metricsDeniedHelpText()
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
: new InMemoryMetricExporter(AggregationTemporality.DELTA);
|
|
202
|
+
return exporter;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Rewrites the export method to include an error handler which logs
|
|
208
|
+
* helpful information about how to set up metrics/telemetry in GCP.
|
|
209
|
+
*/
|
|
210
|
+
class MetricExporterWrapper extends MetricExporter {
|
|
211
|
+
private promise = new Promise<void>((resolve) => resolve());
|
|
212
|
+
|
|
213
|
+
constructor(
|
|
214
|
+
options?: ExporterOptions,
|
|
215
|
+
private errorHandler?: (error: Error) => void
|
|
216
|
+
) {
|
|
217
|
+
super(options);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async export(
|
|
221
|
+
metrics: ResourceMetrics,
|
|
222
|
+
resultCallback: (result: ExportResult) => void
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
await this.promise;
|
|
225
|
+
this.modifyStartTimes(metrics);
|
|
226
|
+
this.promise = new Promise<void>((resolve) => {
|
|
227
|
+
super.export(metrics, (result) => {
|
|
228
|
+
try {
|
|
229
|
+
if (this.errorHandler && result.error) {
|
|
230
|
+
this.errorHandler(result.error);
|
|
231
|
+
}
|
|
232
|
+
resultCallback(result);
|
|
233
|
+
} finally {
|
|
234
|
+
resolve();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
selectAggregation(instrumentType: InstrumentType) {
|
|
241
|
+
if (instrumentType === InstrumentType.HISTOGRAM) {
|
|
242
|
+
return new ExponentialHistogramAggregation();
|
|
243
|
+
}
|
|
244
|
+
return new DefaultAggregation();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
selectAggregationTemporality(instrumentType: InstrumentType) {
|
|
248
|
+
return AggregationTemporality.DELTA;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Modify the start times of each data point to ensure no
|
|
253
|
+
* overlap with previous exports.
|
|
254
|
+
*
|
|
255
|
+
* Cloud metrics do not support delta metrics for custom metrics
|
|
256
|
+
* and will convert any DELTA aggregations to CUMULATIVE ones on
|
|
257
|
+
* export. There is implicit overlap in the start/end times that
|
|
258
|
+
* the Metric reader is sending -- the end_time of the previous
|
|
259
|
+
* export will become the start_time of the current export. The
|
|
260
|
+
* overlap in times means that only one of those records will
|
|
261
|
+
* persist and the other will be overwritten. This
|
|
262
|
+
* method adds a thousandth of a second to ensure discrete export
|
|
263
|
+
* timeframes.
|
|
264
|
+
*/
|
|
265
|
+
private modifyStartTimes(metrics: ResourceMetrics): void {
|
|
266
|
+
metrics.scopeMetrics.forEach((scopeMetric) => {
|
|
267
|
+
scopeMetric.metrics.forEach((metric) => {
|
|
268
|
+
metric.dataPoints.forEach((dataPoint) => {
|
|
269
|
+
dataPoint.startTime[1] = dataPoint.startTime[1] + 1_000_000;
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async shutdown(): Promise<void> {
|
|
276
|
+
return await this.forceFlush();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async forceFlush(): Promise<void> {
|
|
280
|
+
await this.promise;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Adjusts spans before exporting to GCP. Redacts model input
|
|
286
|
+
* and output content, and augments span attributes before sending to GCP.
|
|
287
|
+
*/
|
|
288
|
+
class AdjustingTraceExporter implements SpanExporter {
|
|
289
|
+
constructor(
|
|
290
|
+
private exporter: SpanExporter,
|
|
291
|
+
private logInputAndOutput: boolean,
|
|
292
|
+
private projectId?: string,
|
|
293
|
+
private errorHandler?: (error: Error) => void
|
|
294
|
+
) {}
|
|
295
|
+
|
|
296
|
+
export(
|
|
297
|
+
spans: ReadableSpan[],
|
|
298
|
+
resultCallback: (result: ExportResult) => void
|
|
299
|
+
): void {
|
|
300
|
+
this.exporter?.export(this.adjust(spans), (result) => {
|
|
301
|
+
if (this.errorHandler && result.error) {
|
|
302
|
+
this.errorHandler(result.error);
|
|
303
|
+
}
|
|
304
|
+
resultCallback(result);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
shutdown(): Promise<void> {
|
|
309
|
+
return this.exporter?.shutdown();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
getExporter(): SpanExporter {
|
|
313
|
+
return this.exporter;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
forceFlush(): Promise<void> {
|
|
317
|
+
if (this.exporter?.forceFlush) {
|
|
318
|
+
return this.exporter.forceFlush();
|
|
319
|
+
}
|
|
320
|
+
return Promise.resolve();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private adjust(spans: ReadableSpan[]): ReadableSpan[] {
|
|
324
|
+
return spans.map((span) => {
|
|
325
|
+
this.tickTelemetry(span);
|
|
326
|
+
|
|
327
|
+
span = this.redactInputOutput(span);
|
|
328
|
+
span = this.markErrorSpanAsError(span);
|
|
329
|
+
span = this.markFailedSpan(span);
|
|
330
|
+
span = this.markGenkitFeature(span);
|
|
331
|
+
span = this.markGenkitModel(span);
|
|
332
|
+
span = this.normalizeLabels(span);
|
|
333
|
+
return span;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private tickTelemetry(span: ReadableSpan) {
|
|
338
|
+
const attributes = span.attributes;
|
|
339
|
+
if (!Object.keys(attributes).includes('genkit:type')) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const type = attributes['genkit:type'] as string;
|
|
344
|
+
const subtype = attributes['genkit:metadata:subtype'] as string;
|
|
345
|
+
const isRoot = !!span.attributes['genkit:isRoot'];
|
|
346
|
+
|
|
347
|
+
pathsTelemetry.tick(span, this.logInputAndOutput, this.projectId);
|
|
348
|
+
if (isRoot) {
|
|
349
|
+
// Report top level feature request and latency only for root spans
|
|
350
|
+
// Log input to and output from to the feature
|
|
351
|
+
featuresTelemetry.tick(span, this.logInputAndOutput, this.projectId);
|
|
352
|
+
// Set root status explicitly
|
|
353
|
+
span.attributes['genkit:rootState'] = span.attributes['genkit:state'];
|
|
354
|
+
} else {
|
|
355
|
+
if (type === 'action' && subtype === 'model') {
|
|
356
|
+
// Report generate metrics () for all model actions
|
|
357
|
+
generateTelemetry.tick(span, this.logInputAndOutput, this.projectId);
|
|
358
|
+
}
|
|
359
|
+
if (type === 'action' && subtype === 'tool') {
|
|
360
|
+
// TODO: Report input and output for tool actions
|
|
361
|
+
}
|
|
362
|
+
if (
|
|
363
|
+
type === 'action' ||
|
|
364
|
+
type === 'flow' ||
|
|
365
|
+
type == 'flowStep' ||
|
|
366
|
+
type == 'util'
|
|
367
|
+
) {
|
|
368
|
+
// Report request and latency metrics for all actions
|
|
369
|
+
actionTelemetry.tick(span, this.logInputAndOutput, this.projectId);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (type === 'userEngagement') {
|
|
373
|
+
// Report user acceptance and feedback metrics
|
|
374
|
+
engagementTelemetry.tick(span, this.logInputAndOutput, this.projectId);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private redactInputOutput(span: ReadableSpan): ReadableSpan {
|
|
379
|
+
const hasInput = 'genkit:input' in span.attributes;
|
|
380
|
+
const hasOutput = 'genkit:output' in span.attributes;
|
|
381
|
+
|
|
382
|
+
return !hasInput && !hasOutput
|
|
383
|
+
? span
|
|
384
|
+
: {
|
|
385
|
+
...span,
|
|
386
|
+
spanContext: span.spanContext,
|
|
387
|
+
attributes: {
|
|
388
|
+
...span.attributes,
|
|
389
|
+
'genkit:input': '<redacted>',
|
|
390
|
+
'genkit:output': '<redacted>',
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// This is a workaround for GCP Trace to mark a span with a red
|
|
396
|
+
// exclamation mark indicating that it is an error.
|
|
397
|
+
private markErrorSpanAsError(span: ReadableSpan): ReadableSpan {
|
|
398
|
+
return span.status.code !== SpanStatusCode.ERROR
|
|
399
|
+
? span
|
|
400
|
+
: {
|
|
401
|
+
...span,
|
|
402
|
+
spanContext: span.spanContext,
|
|
403
|
+
attributes: {
|
|
404
|
+
...span.attributes,
|
|
405
|
+
'/http/status_code': '599',
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private normalizeLabels(span: ReadableSpan): ReadableSpan {
|
|
411
|
+
const normalized = {} as Record<string, any>;
|
|
412
|
+
for (const [key, value] of Object.entries(span.attributes)) {
|
|
413
|
+
normalized[key.replace(/\:/g, '/')] = value;
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
...span,
|
|
417
|
+
spanContext: span.spanContext,
|
|
418
|
+
attributes: normalized,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private markFailedSpan(span: ReadableSpan): ReadableSpan {
|
|
423
|
+
if (span.attributes['genkit:isFailureSource']) {
|
|
424
|
+
span.attributes['genkit:failedSpan'] = span.attributes['genkit:name'];
|
|
425
|
+
span.attributes['genkit:failedPath'] = span.attributes['genkit:path'];
|
|
426
|
+
}
|
|
427
|
+
return span;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private markGenkitFeature(span: ReadableSpan): ReadableSpan {
|
|
431
|
+
if (span.attributes['genkit:isRoot'] && !!span.attributes['genkit:name']) {
|
|
432
|
+
span.attributes['genkit:feature'] = span.attributes['genkit:name'];
|
|
433
|
+
}
|
|
434
|
+
return span;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private markGenkitModel(span: ReadableSpan): ReadableSpan {
|
|
438
|
+
if (
|
|
439
|
+
span.attributes['genkit:metadata:subtype'] === 'model' &&
|
|
440
|
+
!!span.attributes['genkit:name']
|
|
441
|
+
) {
|
|
442
|
+
span.attributes['genkit:model'] = span.attributes['genkit:name'];
|
|
443
|
+
}
|
|
444
|
+
return span;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function getErrorHandler(
|
|
449
|
+
shouldLogFn: (err: Error) => boolean,
|
|
450
|
+
helpText: string
|
|
451
|
+
): (err: Error) => void {
|
|
452
|
+
// only log the first time
|
|
453
|
+
let instructionsLogged = false;
|
|
454
|
+
|
|
455
|
+
return (err) => {
|
|
456
|
+
// Use the defaultLogger so that logs don't get swallowed by the open
|
|
457
|
+
// telemetry exporter
|
|
458
|
+
const defaultLogger = logger.defaultLogger;
|
|
459
|
+
if (err && shouldLogFn(err)) {
|
|
460
|
+
if (!instructionsLogged) {
|
|
461
|
+
instructionsLogged = true;
|
|
462
|
+
defaultLogger.error(
|
|
463
|
+
`Unable to send telemetry to Google Cloud: ${err.message}\n\n${helpText}\n`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
} else if (err) {
|
|
467
|
+
defaultLogger.error(`Unable to send telemetry to Google Cloud: ${err}`);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** @hidden */
|
|
473
|
+
export function __getMetricExporterForTesting(): InMemoryMetricExporter {
|
|
474
|
+
return metricExporter as InMemoryMetricExporter;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** @hidden */
|
|
478
|
+
export function __getSpanExporterForTesting(): InMemorySpanExporter {
|
|
479
|
+
return spanExporter.getExporter() as InMemorySpanExporter;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** @hidden */
|
|
483
|
+
export function __forceFlushSpansForTesting() {
|
|
484
|
+
spanProcessor.forceFlush();
|
|
485
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
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 { getCurrentEnv } from 'genkit';
|
|
18
|
+
import { logger } from 'genkit/logging';
|
|
19
|
+
import { enableTelemetry } from 'genkit/tracing';
|
|
20
|
+
import { credentialsFromEnvironment } from './auth.js';
|
|
21
|
+
import { GcpLogger } from './gcpLogger.js';
|
|
22
|
+
import { GcpOpenTelemetry } from './gcpOpenTelemetry.js';
|
|
23
|
+
import { TelemetryConfigs } from './telemetry/defaults.js';
|
|
24
|
+
import type { GcpTelemetryConfig, GcpTelemetryConfigOptions } from './types.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Enables telemetry export to the Google Cloud Observability suite.
|
|
28
|
+
*
|
|
29
|
+
* @param options configuration options
|
|
30
|
+
*/
|
|
31
|
+
export function enableGoogleCloudTelemetry(
|
|
32
|
+
options?: GcpTelemetryConfigOptions
|
|
33
|
+
) {
|
|
34
|
+
return enableTelemetry(
|
|
35
|
+
configureGcpPlugin(options).then(async (pluginConfig) => {
|
|
36
|
+
logger.init(await new GcpLogger(pluginConfig).getLogger(getCurrentEnv()));
|
|
37
|
+
return new GcpOpenTelemetry(pluginConfig).getConfig();
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a configuration object for the plugin.
|
|
44
|
+
* Not normally needed, but exposed for use by the Firebase plugin.
|
|
45
|
+
*/
|
|
46
|
+
async function configureGcpPlugin(
|
|
47
|
+
options?: GcpTelemetryConfigOptions
|
|
48
|
+
): Promise<GcpTelemetryConfig> {
|
|
49
|
+
const envOptions = await credentialsFromEnvironment();
|
|
50
|
+
return {
|
|
51
|
+
projectId: options?.projectId || envOptions.projectId,
|
|
52
|
+
credentials: options?.credentials || envOptions.credentials,
|
|
53
|
+
...TelemetryConfigs.defaults(options),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export * from './gcpLogger.js';
|
|
58
|
+
export * from './gcpOpenTelemetry.js';
|
|
59
|
+
export type { GcpTelemetryConfigOptions } from './types.js';
|
package/src/metrics.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
metrics,
|
|
19
|
+
type Counter,
|
|
20
|
+
type Histogram,
|
|
21
|
+
type Meter,
|
|
22
|
+
} from '@opentelemetry/api';
|
|
23
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
24
|
+
|
|
25
|
+
export const METER_NAME = 'genkit';
|
|
26
|
+
export const METRIC_NAME_PREFIX = 'genkit';
|
|
27
|
+
const METRIC_DIMENSION_MAX_CHARS = 256;
|
|
28
|
+
|
|
29
|
+
export function internalMetricNamespaceWrap(...namespaces) {
|
|
30
|
+
return [METRIC_NAME_PREFIX, ...namespaces].join('/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type MetricCreateFn<T> = (meter: Meter) => T;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Wrapper for OpenTelemetry metrics.
|
|
37
|
+
*
|
|
38
|
+
* The OpenTelemetry {MeterProvider} can only be accessed through the metrics
|
|
39
|
+
* API after the NodeSDK library has been initialized. To prevent race
|
|
40
|
+
* conditions we defer the instantiation of the metric to when it is first
|
|
41
|
+
* ticked.
|
|
42
|
+
*/
|
|
43
|
+
class Metric<T> {
|
|
44
|
+
readonly createFn: MetricCreateFn<T>;
|
|
45
|
+
readonly meterName: string;
|
|
46
|
+
metric?: T;
|
|
47
|
+
|
|
48
|
+
constructor(createFn: MetricCreateFn<T>, meterName: string = METER_NAME) {
|
|
49
|
+
this.meterName = meterName;
|
|
50
|
+
this.createFn = createFn;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get(): T {
|
|
54
|
+
if (!this.metric) {
|
|
55
|
+
this.metric = this.createFn(
|
|
56
|
+
metrics.getMeterProvider().getMeter(this.meterName)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return this.metric;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Wrapper for an OpenTelemetry Counter.
|
|
66
|
+
*
|
|
67
|
+
* By using this wrapper, we defer initialization of the counter until it is
|
|
68
|
+
* need, which ensures that the OpenTelemetry SDK has been initialized before
|
|
69
|
+
* the metric has been defined.
|
|
70
|
+
*/
|
|
71
|
+
export class MetricCounter extends Metric<Counter> {
|
|
72
|
+
constructor(name: string, options: any) {
|
|
73
|
+
super((meter) => meter.createCounter(name, options));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
add(val?: number, opts?: any) {
|
|
77
|
+
if (val) {
|
|
78
|
+
truncateDimensions(opts);
|
|
79
|
+
this.get().add(val, opts);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Wrapper for an OpenTelemetry Histogram.
|
|
86
|
+
*
|
|
87
|
+
* By using this wrapper, we defer initialization of the counter until it is
|
|
88
|
+
* need, which ensures that the OpenTelemetry SDK has been initialized before
|
|
89
|
+
* the metric has been defined.
|
|
90
|
+
*/
|
|
91
|
+
export class MetricHistogram extends Metric<Histogram> {
|
|
92
|
+
constructor(name: string, options: any) {
|
|
93
|
+
super((meter) => meter.createHistogram(name, options));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
record(val?: number, opts?: any) {
|
|
97
|
+
if (val) {
|
|
98
|
+
truncateDimensions(opts);
|
|
99
|
+
this.get().record(val, opts);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Truncates all of the metric dimensions to ensure they are below the trace
|
|
106
|
+
* attribute max. Labels should be consistent between metrics and traces so that
|
|
107
|
+
* data can be properly correlated. This will truncate all dimensions for
|
|
108
|
+
* metrics such that they will be consistent with data included in traces.
|
|
109
|
+
*/
|
|
110
|
+
function truncateDimensions(opts?: any) {
|
|
111
|
+
if (opts) {
|
|
112
|
+
Object.keys(opts).forEach((k) => {
|
|
113
|
+
if (typeof opts[k] === 'string') {
|
|
114
|
+
opts[k] = opts[k].substring(0, METRIC_DIMENSION_MAX_CHARS);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface Telemetry {
|
|
121
|
+
tick(span: ReadableSpan, logIO: boolean, projectId?: string): void;
|
|
122
|
+
}
|