Skip to content

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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/blobs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
},
"dependencies": {
"@netlify/dev-utils": "3.2.2",
"@netlify/otel": "^3.0.2",
"@netlify/runtime-utils": "2.1.0"
}
}
71 changes: 71 additions & 0 deletions packages/blobs/src/store.ts
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:'
Expand Down Expand Up @@ -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
Comment on lines +133 to +139
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh neat, thank u

} 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 @netlify/otel expose it or not, but if needed could be added there). That uses async_hooks to propagate active context/span through concurrent async hell

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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 })

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
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",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
Expand Down
1 change: 1 addition & 0 deletions packages/cache/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"vitest": "^3.0.0"
},
"dependencies": {
"@netlify/otel": "^3.0.2",
"@netlify/runtime-utils": "2.1.0"
}
}
1 change: 1 addition & 0 deletions packages/functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"dependencies": {
"@netlify/blobs": "10.0.4",
"@netlify/dev-utils": "3.2.2",
"@netlify/otel": "^3.0.2",
"@netlify/serverless-functions-api": "2.1.3",
"@netlify/zip-it-and-ship-it": "^12.2.0",
"cron-parser": "^4.9.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/otel/src/bootstrap/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const createTracerProvider = async (options: {
const runtimeVersion = nodeVersion.slice(1)

const { Resource } = await import('@opentelemetry/resources')
const { NodeTracerProvider, BatchSpanProcessor } = await import('@opentelemetry/sdk-trace-node')
const { NodeTracerProvider, SimpleSpanProcessor } = await import('@opentelemetry/sdk-trace-node')

const { NetlifySpanExporter } = await import('./netlify_span_exporter.js')

Expand All @@ -34,7 +34,7 @@ export const createTracerProvider = async (options: {

const nodeTracerProvider = new NodeTracerProvider({
resource,
spanProcessors: [new BatchSpanProcessor(new NetlifySpanExporter())],
spanProcessors: [new SimpleSpanProcessor(new NetlifySpanExporter())],
})

nodeTracerProvider.register()
Expand Down
Loading