-
Notifications
You must be signed in to change notification settings - Fork 3
Add opentelemetry instrumentation to blobs methods #338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
005520e
ad8d299
61bdfcc
eb7b095
0bd0182
cc108c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,13 @@ | ||
import { getTracer } from '@netlify/otel' | ||
import { 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 { Attributes, Span, SpanStatusCode } from '@opentelemetry/api' | ||
|
||
export const DEPLOY_STORE_PREFIX = 'deploy:' | ||
export const LEGACY_STORE_INTERNAL_PREFIX = 'netlify-internal/legacy-namespace/' | ||
export const SITE_STORE_PREFIX = 'site:' | ||
|
@@ -105,11 +108,51 @@ export type WriteResult = { | |
modified: boolean | ||
} | ||
|
||
function otel<This extends Store, Args extends any[], Return>(getAttributes?: (...args: Args) => Attributes) { | ||
return function (method: (...args: Args) => Promise<Return>, context: ClassMethodDecoratorContext) { | ||
const methodName = String(context.name) | ||
const operationName = `blobs.${methodName}` | ||
return async function (this: This, ...args: Args): Promise<Return> { | ||
const storeName = this['name'] | ||
const tracer = await getTracer() | ||
|
||
if (tracer) { | ||
return tracer.withActiveSpan(operationName, async (span) => { | ||
span.setAttribute('blobs.store', storeName) | ||
if (getAttributes) { | ||
for (const [name, value] of Object.entries(getAttributes(...args))) { | ||
value && span.setAttribute(`blobs.${name}`, value) | ||
} | ||
} | ||
try { | ||
this['span'] = span | ||
const result = await method.apply(this, args) | ||
span.setStatus({ code: SpanStatusCode.OK }) | ||
return result | ||
} catch (error) { | ||
const message = (error as Error).message ?? 'problem' | ||
span.setStatus({ | ||
code: SpanStatusCode.ERROR, | ||
message, | ||
}) | ||
span.recordException(error as Error) | ||
throw error | ||
} finally { | ||
this['span'] = null | ||
} | ||
}) | ||
} | ||
return await method.apply(this, args) | ||
} | ||
} | ||
} | ||
|
||
export type BlobResponseType = 'arrayBuffer' | 'blob' | 'json' | 'stream' | 'text' | ||
|
||
export class Store { | ||
private client: Client | ||
private name: string | ||
private span: Span | null = null | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this currently unused, but generally you wouldn't want to store a span like that because single blobs store can have multiple concurrent operations going on and this wouldn't work for such scenarios. If you need to get some active span - https://opentelemetry.io/docs/languages/js/instrumentation/#get-the-current-span is the way to go (not sure if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks, i will look into that api |
||
|
||
constructor(options: StoreOptions) { | ||
this.client = options.client | ||
|
@@ -153,6 +196,11 @@ export class Store { | |
async get(key: string, { type }: GetOptions & { type: 'json' }): Promise<any> | ||
async get(key: string, { type }: GetOptions & { type: 'stream' }): Promise<ReadableStream> | ||
async get(key: string, { type }: GetOptions & { type: 'text' }): Promise<string> | ||
@otel((key, options) => ({ | ||
method: 'GET', | ||
key, | ||
response_type: options?.type ?? 'text', | ||
})) | ||
async get( | ||
key: string, | ||
options?: GetOptions & { type?: BlobResponseType }, | ||
|
@@ -191,6 +239,11 @@ export class Store { | |
throw new BlobsInternalError(res) | ||
} | ||
|
||
@otel((key, options) => ({ | ||
method: 'GET', | ||
key, | ||
consistency: options?.consistency, | ||
})) | ||
async getMetadata(key: string, { consistency }: { consistency?: ConsistencyMode } = {}) { | ||
const res = await this.client.makeRequest({ consistency, key, method: HTTPMethod.HEAD, storeName: this.name }) | ||
|
||
|
@@ -242,6 +295,12 @@ export class Store { | |
options: { type: 'text' } & GetWithMetadataOptions, | ||
): Promise<({ data: string } & GetWithMetadataResult) | null> | ||
|
||
@otel((key, options) => ({ | ||
method: 'GET', | ||
key, | ||
consistency: options?.consistency, | ||
response_type: options?.type ?? 'text', | ||
})) | ||
async getWithMetadata( | ||
key: string, | ||
options?: { type: BlobResponseType } & GetWithMetadataOptions, | ||
|
@@ -325,6 +384,12 @@ export class Store { | |
) | ||
} | ||
|
||
@otel((key, data, options) => ({ | ||
method: 'PUT', | ||
key, | ||
atomic: options?.onlyIfMatch || options?.onlyIfNew, | ||
type: typeof data == 'string' ? 'text' : data instanceof Blob ? data.type : 'arrayBuffer', | ||
})) | ||
async set(key: string, data: BlobInput, options: SetOptions = {}): Promise<WriteResult> { | ||
Store.validateKey(key) | ||
|
||
|
@@ -353,6 +418,12 @@ export class Store { | |
throw new BlobsInternalError(res) | ||
} | ||
|
||
@otel((key, data, options) => ({ | ||
method: 'PUT', | ||
key, | ||
atomic: options?.onlyIfMatch || options?.onlyIfNew, | ||
type: 'json', | ||
})) | ||
async setJSON(key: string, data: unknown, options: SetOptions = {}): Promise<WriteResult> { | ||
Store.validateKey(key) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -75,6 +75,7 @@ | |
"vitest": "^3.0.0" | ||
}, | ||
"dependencies": { | ||
"@netlify/otel": "^3.0.2", | ||
"@netlify/runtime-utils": "2.1.0" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SugaredTracer
we get from@netlify/otel
will already automatically handle recording errors ( https://github.com/open-telemetry/opentelemetry-js/blob/41ba7f57cbf5ae22290168188b467e0c60cd4765/api/src/experimental/trace/SugaredTracer.ts#L19-L24 )There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh neat, thank u