-
Notifications
You must be signed in to change notification settings - Fork 15
XTNSNS-1082 Update metrics instrumentation #596
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
Merged
Merged
Changes from all commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
df51513
Bump diagnostics-nodejs version
daniyelnnr aa2e3bb
Refactor telemetry client to support all signals
daniyelnnr 35428ee
Refactor getLogClient to simplify initialization
daniyelnnr 536e16c
Refactor logger client types
daniyelnnr b5b2425
Add resolution for @grpc/grpc-js dependency
daniyelnnr ec13074
Update package version to 6.49.8-beta.0
daniyelnnr 00afbcb
Refactor singleton to use dedicated initialization methods for teleme…
daniyelnnr f3d9c05
Refactor telemetry client initialization logic
daniyelnnr 58d03c9
Update package.json and yarn.lock
daniyelnnr cfac3a1
Update startApp function to proper init telemetry
daniyelnnr 48127f3
Add metrics client
daniyelnnr e10016d
Add metrics instruments for monitoring HTTP requests
daniyelnnr c1d29a9
Add middleware for request metrics
daniyelnnr 4549879
Add middleware usage on app
daniyelnnr cf53947
Add Koa instrumentation to telemetry client
daniyelnnr 7faeac7
Add Koa context propagation middleware to app worker
daniyelnnr 41f7dee
Add host-metrics instrumentation
daniyelnnr 5b5146d
Add host-metrics instrumentation to telemetry client
daniyelnnr 8b715bd
Merge branch 'master' into chore/bump-diagnostics
daniyelnnr 6503f4d
Release v7.0.1
daniyelnnr c9bde12
Merge branch 'chore/bump-diagnostics' into update/metrics
daniyelnnr ff3bbb9
Release v7.1.0-beta.0
daniyelnnr b54e413
Improves code formatting for setTimeout
daniyelnnr b596cc7
Refactor instrument init logic on middleware
daniyelnnr 08bef4e
Refactor metric client module
daniyelnnr 1477054
Refactor metrics instruments module
daniyelnnr 6733ee2
Add error handling when init instruments
daniyelnnr b0c660b
Merge branch 'master' into update/metrics
daniyelnnr 85c7a57
Release v7.1.0
daniyelnnr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { Types } from "@vtex/diagnostics-nodejs"; | ||
| import { initializeTelemetry } from '../telemetry'; | ||
|
|
||
| class MetricClientSingleton { | ||
| private static instance: MetricClientSingleton | undefined; | ||
| private client: Types.MetricClient | undefined; | ||
| private initPromise: Promise<Types.MetricClient> | undefined; | ||
|
|
||
| private constructor() {} | ||
|
|
||
| public static getInstance(): MetricClientSingleton { | ||
| if (!MetricClientSingleton.instance) { | ||
| MetricClientSingleton.instance = new MetricClientSingleton(); | ||
| } | ||
| return MetricClientSingleton.instance; | ||
| } | ||
|
|
||
| public async getClient(): Promise<Types.MetricClient> { | ||
| if (this.client) { | ||
| return this.client; | ||
| } | ||
|
|
||
| if (this.initPromise) { | ||
| return this.initPromise; | ||
| } | ||
|
|
||
| this.initPromise = this.initializeClient(); | ||
|
|
||
| return this.initPromise; | ||
| } | ||
|
|
||
| private async initializeClient(): Promise<Types.MetricClient> { | ||
| try { | ||
| const { metricsClient } = await initializeTelemetry(); | ||
| this.client = metricsClient; | ||
| this.initPromise = undefined; | ||
| return metricsClient; | ||
| } catch (error) { | ||
| console.error('Failed to initialize metrics client:', error); | ||
| this.initPromise = undefined; | ||
| throw error; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export const getMetricClient = () => MetricClientSingleton.getInstance().getClient(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { InstrumentationBase, InstrumentationConfig } from "@opentelemetry/instrumentation"; | ||
| import { MeterProvider } from '@opentelemetry/api'; | ||
| import { HostMetrics } from "@opentelemetry/host-metrics"; | ||
|
|
||
| interface HostMetricsInstrumentationConfig extends InstrumentationConfig { | ||
| name?: string; | ||
| meterProvider?: MeterProvider; | ||
| } | ||
|
|
||
| export class HostMetricsInstrumentation extends InstrumentationBase<HostMetricsInstrumentationConfig> { | ||
| private hostMetrics?: HostMetrics; | ||
|
|
||
| constructor(config: HostMetricsInstrumentationConfig = {}) { | ||
| const instrumentation_name = config.name || 'host-metrics-instrumentation'; | ||
| const instrumentation_version = '1.0.0'; | ||
| super(instrumentation_name, instrumentation_version, config); | ||
| } | ||
|
|
||
| init(): void {} | ||
|
|
||
| enable(): void { | ||
| if (!this._config.meterProvider) { | ||
| throw new Error('MeterProvider is required for HostMetricsInstrumentation'); | ||
| } | ||
|
|
||
| this.hostMetrics = new HostMetrics({ | ||
| meterProvider: this._config.meterProvider, | ||
| name: this._config.name || 'host-metrics', | ||
| }); | ||
|
|
||
| this.hostMetrics.start(); | ||
| console.debug('HostMetricsInstrumentation enabled'); | ||
| } | ||
|
|
||
| disable(): void { | ||
| if (this.hostMetrics) { | ||
| this.hostMetrics = undefined; | ||
| console.debug('HostMetricsInstrumentation disabled'); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import { Types } from '@vtex/diagnostics-nodejs' | ||
| import { getMetricClient } from './client' | ||
|
|
||
| export const enum RequestsMetricLabels { | ||
| STATUS_CODE = 'status_code', | ||
| REQUEST_HANDLER = 'handler', | ||
| } | ||
|
|
||
| export interface OtelRequestInstruments { | ||
| concurrentRequests: Types.Gauge | ||
| requestTimings: Types.Histogram | ||
| totalRequests: Types.Counter | ||
| responseSizes: Types.Histogram | ||
| abortedRequests: Types.Counter | ||
| } | ||
|
|
||
| const createOtelConcurrentRequestsInstrument = async (): Promise<Types.Gauge> => { | ||
| const metricsClient = await getMetricClient() | ||
| return metricsClient.createGauge('io_http_requests_current', { | ||
| description: 'The current number of requests in course.', | ||
| unit: '1' | ||
| }) | ||
| } | ||
|
|
||
| const createOtelRequestsTimingsInstrument = async (): Promise<Types.Histogram> => { | ||
| const metricsClient = await getMetricClient() | ||
| return metricsClient.createHistogram('runtime_http_requests_duration_milliseconds', { | ||
| description: 'The incoming http requests total duration.', | ||
| unit: 'ms' | ||
| }) | ||
| } | ||
|
|
||
| const createOtelTotalRequestsInstrument = async (): Promise<Types.Counter> => { | ||
| const metricsClient = await getMetricClient() | ||
| return metricsClient.createCounter('runtime_http_requests_total', { | ||
| description: 'The total number of HTTP requests.', | ||
| unit: '1' | ||
| }) | ||
| } | ||
|
|
||
| const createOtelRequestsResponseSizesInstrument = async (): Promise<Types.Histogram> => { | ||
| const metricsClient = await getMetricClient() | ||
| return metricsClient.createHistogram('runtime_http_response_size_bytes', { | ||
| description: 'The outgoing response sizes (only applicable when the response isn\'t a stream).', | ||
| unit: 'bytes' | ||
| }) | ||
| } | ||
|
|
||
| const createOtelTotalAbortedRequestsInstrument = async (): Promise<Types.Counter> => { | ||
| const metricsClient = await getMetricClient() | ||
| return metricsClient.createCounter('runtime_http_aborted_requests_total', { | ||
| description: 'The total number of HTTP requests aborted.', | ||
| unit: '1' | ||
| }) | ||
| } | ||
|
|
||
| class OtelInstrumentsSingleton { | ||
| private static instance: OtelInstrumentsSingleton | undefined; | ||
| private instruments: OtelRequestInstruments | undefined; | ||
| private initializingPromise: Promise<OtelRequestInstruments> | undefined; | ||
|
|
||
| private constructor() {} | ||
|
|
||
| public static getInstance(): OtelInstrumentsSingleton { | ||
| if (!OtelInstrumentsSingleton.instance) { | ||
| OtelInstrumentsSingleton.instance = new OtelInstrumentsSingleton(); | ||
| } | ||
| return OtelInstrumentsSingleton.instance; | ||
| } | ||
|
|
||
| public async getInstruments(): Promise<OtelRequestInstruments> { | ||
| if (this.instruments) { | ||
| return this.instruments; | ||
| } | ||
|
|
||
| if (this.initializingPromise) { | ||
| return this.initializingPromise; | ||
| } | ||
|
|
||
| this.initializingPromise = this.initializeInstruments(); | ||
|
|
||
| try { | ||
| this.instruments = await this.initializingPromise; | ||
| return this.instruments; | ||
| } catch (error) { | ||
| console.error('Failed to initialize OTel instruments:', error); | ||
| this.initializingPromise = undefined; | ||
| throw error; | ||
| } finally { | ||
| this.initializingPromise = undefined; | ||
| } | ||
| } | ||
|
|
||
| private async initializeInstruments(): Promise<OtelRequestInstruments> { | ||
| const [ | ||
| concurrentRequests, | ||
| requestTimings, | ||
| totalRequests, | ||
| responseSizes, | ||
| abortedRequests | ||
| ] = await Promise.all([ | ||
| createOtelConcurrentRequestsInstrument(), | ||
| createOtelRequestsTimingsInstrument(), | ||
| createOtelTotalRequestsInstrument(), | ||
| createOtelRequestsResponseSizesInstrument(), | ||
| createOtelTotalAbortedRequestsInstrument() | ||
| ]) | ||
|
|
||
| return { | ||
| concurrentRequests, | ||
| requestTimings, | ||
| totalRequests, | ||
| responseSizes, | ||
| abortedRequests | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export const getOtelInstruments = () => OtelInstrumentsSingleton.getInstance().getInstruments(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import { finished as onStreamFinished } from 'stream' | ||
| import { hrToMillisFloat } from '../../utils' | ||
| import { getOtelInstruments, RequestsMetricLabels, OtelRequestInstruments } from './metrics' | ||
| import { ServiceContext } from '../worker/runtime/typings' | ||
|
|
||
| const INSTRUMENTS_INITIALIZATION_TIMEOUT = 500 | ||
|
|
||
| export const addOtelRequestMetricsMiddleware = () => { | ||
| let instruments: OtelRequestInstruments | undefined | ||
|
|
||
| const tryGetInstruments = async (ctx: ServiceContext): Promise<OtelRequestInstruments | undefined> => { | ||
| try { | ||
| return await Promise.race([ | ||
| getOtelInstruments(), | ||
| new Promise<never>((_, reject) => | ||
| setTimeout( | ||
| () => reject(new Error('Timeout waiting for OpenTelemetry instruments initialization')), | ||
| INSTRUMENTS_INITIALIZATION_TIMEOUT | ||
| ) | ||
| ) | ||
| ]) | ||
| } catch (error) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error) | ||
| console.warn(`OpenTelemetry instruments not ready for request ${ctx.requestHandlerName}: ${errorMessage}`) | ||
| return undefined | ||
| } | ||
| } | ||
|
|
||
| return async function addOtelRequestMetrics(ctx: ServiceContext, next: () => Promise<void>) { | ||
| instruments = instruments ? instruments : await tryGetInstruments(ctx) | ||
| if (!instruments) { | ||
| await next() | ||
| return | ||
| } | ||
|
|
||
| const start = process.hrtime() | ||
| instruments.concurrentRequests.add(1) | ||
|
|
||
| ctx.req.once('aborted', () => { | ||
| if (instruments) { | ||
| instruments.abortedRequests.add(1, { [RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName }) | ||
| } | ||
| }) | ||
|
|
||
| let responseClosed = false | ||
| ctx.res.once('close', () => (responseClosed = true)) | ||
|
|
||
| try { | ||
| await next() | ||
| } finally { | ||
| const responseLength = ctx.response.length | ||
| if (responseLength && instruments) { | ||
| instruments.responseSizes.record( | ||
| responseLength, | ||
| { [RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName } | ||
| ) | ||
| } | ||
|
|
||
| if (instruments) { | ||
| instruments.totalRequests.add( | ||
| 1, | ||
| { | ||
| [RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName, | ||
| [RequestsMetricLabels.STATUS_CODE]: ctx.response.status, | ||
| } | ||
| ) | ||
| } | ||
|
|
||
| const onResFinished = () => { | ||
| if (instruments) { | ||
| instruments.requestTimings.record( | ||
| hrToMillisFloat(process.hrtime(start)), | ||
| { | ||
| [RequestsMetricLabels.REQUEST_HANDLER]: ctx.requestHandlerName, | ||
| } | ||
| ) | ||
|
|
||
| instruments.concurrentRequests.subtract(1) | ||
| } | ||
| } | ||
|
|
||
| if (responseClosed) { | ||
| onResFinished() | ||
| } else { | ||
| onStreamFinished(ctx.res, onResFinished) | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.