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