diff --git a/packages/blobs/src/store.ts b/packages/blobs/src/store.ts index c34baa699..f3dcec98a 100644 --- a/packages/blobs/src/store.ts +++ b/packages/blobs/src/store.ts @@ -1,11 +1,11 @@ -import { getTracer, withActiveSpan } from '@netlify/otel' +import type { Span } from '@netlify/otel/opentelemetry' import type { DeleteStoreResponse } from './backend/delete_store.ts' import type { ListResponse, ListResponseBlob } from './backend/list.ts' import { Client, type Conditions } from './client.ts' import type { ConsistencyMode } from './consistency.ts' import { getMetadataFromResponse, Metadata } from './metadata.ts' import { BlobInput, HTTPMethod } from './types.ts' -import { BlobsInternalError, collectIterator } from './util.ts' +import { BlobsInternalError, collectIterator, withSpan } from './util.ts' export const DEPLOY_STORE_PREFIX = 'deploy:' export const LEGACY_STORE_INTERNAL_PREFIX = 'netlify-internal/legacy-namespace/' @@ -34,6 +34,10 @@ export interface GetOptions { consistency?: ConsistencyMode } +export interface GetMetadataOptions { + consistency?: ConsistencyMode +} + export interface GetWithMetadataOptions { consistency?: ConsistencyMode etag?: string @@ -60,6 +64,10 @@ export interface ListOptions { prefix?: string } +export interface TracingOptions { + span?: Span +} + export interface DeleteStoreResult { deletedBlobs: number } @@ -177,17 +185,17 @@ export class Store { } } - async get(key: string, options?: GetOptions & { type?: 'arrayBuffer' }): Promise - async get(key: string, options?: GetOptions & { type?: 'blob' }): Promise - async get(key: string, options?: GetOptions & { type?: 'json' }): Promise - async get(key: string, options?: GetOptions & { type?: 'stream' }): Promise - async get(key: string, options?: GetOptions & { type?: 'text' }): Promise - async get(key: string, options?: GetOptions): Promise + async get(key: string, options?: GetOptions & TracingOptions & { type?: 'arrayBuffer' }): Promise + async get(key: string, options?: GetOptions & TracingOptions & { type?: 'blob' }): Promise + async get(key: string, options?: GetOptions & TracingOptions & { type?: 'json' }): Promise + async get(key: string, options?: GetOptions & TracingOptions & { type?: 'stream' }): Promise + async get(key: string, options?: GetOptions & TracingOptions & { type?: 'text' }): Promise + async get(key: string, options?: GetOptions & TracingOptions): Promise async get( key: string, - options?: GetOptions & { type?: BlobResponseType }, + options?: GetOptions & TracingOptions & { type?: BlobResponseType }, ): Promise { - return withActiveSpan(getTracer(), 'blobs.get', async (span) => { + return withSpan(options?.span, 'blobs.get', async (span) => { const { consistency, type } = options ?? {} span?.setAttributes({ @@ -243,15 +251,22 @@ export class Store { }) } - async getMetadata(key: string, { consistency }: { consistency?: ConsistencyMode } = {}) { - return withActiveSpan(getTracer(), 'blobs.getMetadata', async (span) => { + async getMetadata(key: string, options: GetMetadataOptions & TracingOptions = {}) { + return withSpan(options?.span, 'blobs.getMetadata', async (span) => { span?.setAttributes({ 'blobs.store': this.name, 'blobs.key': key, 'blobs.method': 'HEAD', - 'blobs.consistency': consistency, + 'blobs.consistency': options.consistency, }) - const res = await this.client.makeRequest({ consistency, key, method: HTTPMethod.HEAD, storeName: this.name }) + + const res = await this.client.makeRequest({ + consistency: options.consistency, + key, + method: HTTPMethod.HEAD, + storeName: this.name, + }) + span?.setAttributes({ 'blobs.response.status': res.status, }) @@ -277,44 +292,44 @@ export class Store { async getWithMetadata( key: string, - options?: GetWithMetadataOptions, + options?: GetWithMetadataOptions & TracingOptions, ): Promise<({ data: string } & GetWithMetadataResult) | null> async getWithMetadata( key: string, - options: { type: 'arrayBuffer' } & GetWithMetadataOptions, + options: { type: 'arrayBuffer' } & GetWithMetadataOptions & TracingOptions, ): Promise<{ data: ArrayBuffer } & GetWithMetadataResult> async getWithMetadata( key: string, - options: { type: 'blob' } & GetWithMetadataOptions, + options: { type: 'blob' } & GetWithMetadataOptions & TracingOptions, ): Promise<({ data: Blob } & GetWithMetadataResult) | null> async getWithMetadata( key: string, - options: { type: 'json' } & GetWithMetadataOptions, + options: { type: 'json' } & GetWithMetadataOptions & TracingOptions, ): Promise<({ data: any } & GetWithMetadataResult) | null> async getWithMetadata( key: string, - options: { type: 'stream' } & GetWithMetadataOptions, + options: { type: 'stream' } & GetWithMetadataOptions & TracingOptions, ): Promise<({ data: ReadableStream } & GetWithMetadataResult) | null> async getWithMetadata( key: string, - options: { type: 'text' } & GetWithMetadataOptions, + options: { type: 'text' } & GetWithMetadataOptions & TracingOptions, ): Promise<({ data: string } & GetWithMetadataResult) | null> async getWithMetadata( key: string, - options?: { type: BlobResponseType } & GetWithMetadataOptions, + options?: { type: BlobResponseType } & GetWithMetadataOptions & TracingOptions, ): Promise< | ({ data: ArrayBuffer | Blob | ReadableStream | string | null } & GetWithMetadataResult) | null > { - return withActiveSpan(getTracer(), 'blobs.getWithMetadata', async (span) => { + return withSpan(options?.span, 'blobs.getWithMetadata', async (span) => { const { consistency, etag: requestETag, type } = options ?? {} const headers = requestETag ? { 'if-none-match': requestETag } : undefined @@ -384,10 +399,10 @@ export class Store { }) } - list(options: ListOptions & { paginate: true }): AsyncIterable - list(options?: ListOptions & { paginate?: false }): Promise - list(options: ListOptions = {}): Promise | AsyncIterable { - return withActiveSpan(getTracer(), 'blobs.list', (span) => { + list(options: ListOptions & TracingOptions & { paginate: true }): AsyncIterable + list(options?: ListOptions & TracingOptions & { paginate?: false }): Promise + list(options: ListOptions & TracingOptions = {}): Promise | AsyncIterable { + return withSpan(options.span, 'blobs.list', (span) => { span?.setAttributes({ 'blobs.store': this.name, 'blobs.method': 'GET', @@ -414,8 +429,8 @@ export class Store { }) } - async set(key: string, data: BlobInput, options: SetOptions = {}): Promise { - return withActiveSpan(getTracer(), 'blobs.set', async (span) => { + async set(key: string, data: BlobInput, options: SetOptions & TracingOptions = {}): Promise { + return withSpan(options.span, 'blobs.set', async (span) => { span?.setAttributes({ 'blobs.store': this.name, 'blobs.key': key, @@ -459,8 +474,8 @@ export class Store { }) } - async setJSON(key: string, data: unknown, options: SetOptions = {}): Promise { - return withActiveSpan(getTracer(), 'blobs.setJSON', async (span) => { + async setJSON(key: string, data: unknown, options: SetOptions & TracingOptions = {}): Promise { + return withSpan(options.span, 'blobs.setJSON', async (span) => { span?.setAttributes({ 'blobs.store': this.name, 'blobs.key': key, @@ -585,7 +600,7 @@ export class Store { } } - private getListIterator(options?: ListOptions): AsyncIterable { + private getListIterator(options?: ListOptions & TracingOptions): AsyncIterable { const { client, name: storeName } = this const parameters: Record = {} @@ -604,7 +619,7 @@ export class Store { return { async next() { - return withActiveSpan(getTracer(), 'blobs.list.next', async (span) => { + return withSpan(options?.span, 'blobs.list.next', async (span) => { span?.setAttributes({ 'blobs.store': storeName, 'blobs.method': 'GET', diff --git a/packages/blobs/src/util.ts b/packages/blobs/src/util.ts index 160a2edc0..3aa1a97cf 100644 --- a/packages/blobs/src/util.ts +++ b/packages/blobs/src/util.ts @@ -1,4 +1,7 @@ import process from 'node:process' +import { getTracer, withActiveSpan } from '@netlify/otel' +import type { Span } from '@netlify/otel/opentelemetry' + import { NF_ERROR, NF_REQUEST_ID } from './headers.ts' export class BlobsInternalError extends Error { @@ -63,3 +66,12 @@ export function encodeName(string: string): string { export function decodeName(string: string): string { return process.platform == 'win32' ? decodeWin32SafeName(string) : string } + +// Allow users to pass in their own active span or defaults to creating a new active span +export function withSpan ReturnType>(span: Span | undefined, name: string, fn: F) { + if (span) return fn(span) + + return withActiveSpan(getTracer(), name, (span) => { + return fn(span) + }) +} diff --git a/packages/blobs/tsconfig.json b/packages/blobs/tsconfig.json index 7fedf6ee9..83e9a4b76 100644 --- a/packages/blobs/tsconfig.json +++ b/packages/blobs/tsconfig.json @@ -3,7 +3,7 @@ "allowImportingTsExtensions": true, "emitDeclarationOnly": true, "target": "ES2020", - "module": "es2020", + "module": "nodenext", "allowJs": true, "declaration": true, "declarationMap": false, @@ -11,7 +11,7 @@ "outDir": "./dist", "removeComments": false, "strict": true, - "moduleResolution": "node", + "moduleResolution": "nodenext", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true