Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 47 additions & 32 deletions packages/blobs/src/store.ts
Original file line number Diff line number Diff line change
@@ -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/'
Expand Down Expand Up @@ -34,6 +34,10 @@ export interface GetOptions {
consistency?: ConsistencyMode
}

export interface GetMetadataOptions {
consistency?: ConsistencyMode
}

export interface GetWithMetadataOptions {
consistency?: ConsistencyMode
etag?: string
Expand All @@ -60,6 +64,10 @@ export interface ListOptions {
prefix?: string
}

export interface TracingOptions {
span?: Span
}

export interface DeleteStoreResult {
deletedBlobs: number
}
Expand Down Expand Up @@ -177,17 +185,17 @@ export class Store {
}
}

async get(key: string, options?: GetOptions & { type?: 'arrayBuffer' }): Promise<ArrayBuffer>
async get(key: string, options?: GetOptions & { type?: 'blob' }): Promise<Blob>
async get(key: string, options?: GetOptions & { type?: 'json' }): Promise<any>
async get(key: string, options?: GetOptions & { type?: 'stream' }): Promise<ReadableStream>
async get(key: string, options?: GetOptions & { type?: 'text' }): Promise<string>
async get(key: string, options?: GetOptions): Promise<string | null>
async get(key: string, options?: GetOptions & TracingOptions & { type?: 'arrayBuffer' }): Promise<ArrayBuffer>
async get(key: string, options?: GetOptions & TracingOptions & { type?: 'blob' }): Promise<Blob>
async get(key: string, options?: GetOptions & TracingOptions & { type?: 'json' }): Promise<any>
async get(key: string, options?: GetOptions & TracingOptions & { type?: 'stream' }): Promise<ReadableStream>
async get(key: string, options?: GetOptions & TracingOptions & { type?: 'text' }): Promise<string>
async get(key: string, options?: GetOptions & TracingOptions): Promise<string | null>
async get(
key: string,
options?: GetOptions & { type?: BlobResponseType },
options?: GetOptions & TracingOptions & { type?: BlobResponseType },
): Promise<ArrayBuffer | Blob | ReadableStream | string | null> {
return withActiveSpan(getTracer(), 'blobs.get', async (span) => {
return withSpan(options?.span, 'blobs.get', async (span) => {
const { consistency, type } = options ?? {}

span?.setAttributes({
Expand Down Expand Up @@ -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,
})
Expand All @@ -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

Expand Down Expand Up @@ -384,10 +399,10 @@ export class Store {
})
}

list(options: ListOptions & { paginate: true }): AsyncIterable<ListResult>
list(options?: ListOptions & { paginate?: false }): Promise<ListResult>
list(options: ListOptions = {}): Promise<ListResult> | AsyncIterable<ListResult> {
return withActiveSpan(getTracer(), 'blobs.list', (span) => {
list(options: ListOptions & TracingOptions & { paginate: true }): AsyncIterable<ListResult>
list(options?: ListOptions & TracingOptions & { paginate?: false }): Promise<ListResult>
list(options: ListOptions & TracingOptions = {}): Promise<ListResult> | AsyncIterable<ListResult> {
return withSpan(options.span, 'blobs.list', (span) => {
span?.setAttributes({
'blobs.store': this.name,
'blobs.method': 'GET',
Expand All @@ -414,8 +429,8 @@ export class Store {
})
}

async set(key: string, data: BlobInput, options: SetOptions = {}): Promise<WriteResult> {
return withActiveSpan(getTracer(), 'blobs.set', async (span) => {
async set(key: string, data: BlobInput, options: SetOptions & TracingOptions = {}): Promise<WriteResult> {
return withSpan(options.span, 'blobs.set', async (span) => {
span?.setAttributes({
'blobs.store': this.name,
'blobs.key': key,
Expand Down Expand Up @@ -459,8 +474,8 @@ export class Store {
})
}

async setJSON(key: string, data: unknown, options: SetOptions = {}): Promise<WriteResult> {
return withActiveSpan(getTracer(), 'blobs.setJSON', async (span) => {
async setJSON(key: string, data: unknown, options: SetOptions & TracingOptions = {}): Promise<WriteResult> {
return withSpan(options.span, 'blobs.setJSON', async (span) => {
span?.setAttributes({
'blobs.store': this.name,
'blobs.key': key,
Expand Down Expand Up @@ -585,7 +600,7 @@ export class Store {
}
}

private getListIterator(options?: ListOptions): AsyncIterable<ListResult> {
private getListIterator(options?: ListOptions & TracingOptions): AsyncIterable<ListResult> {
const { client, name: storeName } = this
const parameters: Record<string, string> = {}

Expand All @@ -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',
Expand Down
12 changes: 12 additions & 0 deletions packages/blobs/src/util.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<F extends (span?: Span) => ReturnType<F>>(span: Span | undefined, name: string, fn: F) {
if (span) return fn(span)

return withActiveSpan(getTracer(), name, (span) => {
return fn(span)
})
}
4 changes: 2 additions & 2 deletions packages/blobs/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
"allowImportingTsExtensions": true,
"emitDeclarationOnly": true,
"target": "ES2020",
"module": "es2020",
"module": "nodenext",
"allowJs": true,
"declaration": true,
"declarationMap": false,
"sourceMap": false,
"outDir": "./dist",
"removeComments": false,
"strict": true,
"moduleResolution": "node",
"moduleResolution": "nodenext",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to support import from @netlify/otel/opentelemetry

"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
Expand Down