Skip to content
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
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vtex/api",
"version": "6.49.7",
"version": "6.49.8-beta.2",
"description": "VTEX I/O API client",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down Expand Up @@ -47,9 +47,13 @@
},
"license": "MIT",
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/host-metrics": "0.35.5",
"@opentelemetry/instrumentation": "0.57.2",
"@opentelemetry/instrumentation-koa": "0.47.1",
"@types/koa": "^2.11.0",
"@types/koa-compose": "^3.2.3",
"@vtex/diagnostics-nodejs": "0.1.0-beta.10",
"@vtex/diagnostics-nodejs": "0.1.0-io-beta.19",
"@vtex/node-error-report": "^0.0.3",
"@wry/equality": "^0.1.9",
"agentkeepalive": "^4.0.2",
Expand Down Expand Up @@ -124,5 +128,8 @@
"typemoq": "^2.1.0",
"typescript": "^4.4.4",
"typescript-json-schema": "^0.52.0"
},
"resolutions": {
"@grpc/grpc-js": "^1.13.4"
}
}
11 changes: 7 additions & 4 deletions src/service/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import { initializeTelemetry } from './telemetry'
import cluster from 'cluster'

import { HTTP_SERVER_PORT } from '../constants'
import { getServiceJSON } from './loaders'
import { LogLevel, logOnceToDevConsole } from './logger'
import { startMaster } from './master'
import { startWorker } from './worker'

export const startApp = () => {
export const startApp = async () => {
await initializeTelemetry()
const serviceJSON = getServiceJSON()
try {
// if it is a master process then call setting up worker process
if(cluster.isMaster) {
const { startMaster } = await import('./master')
startMaster(serviceJSON)
} else {
// to setup server configurations and share port address for incoming requests
startWorker(serviceJSON).listen(HTTP_SERVER_PORT)
const { startWorker } = await import('./worker')
const app = await startWorker(serviceJSON)
app.listen(HTTP_SERVER_PORT)
}
} catch (err: any) {
logOnceToDevConsole(err.stack || err.message, LogLevel.Error)
Expand Down
45 changes: 14 additions & 31 deletions src/service/logger/client.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,35 @@
import { Exporters } from '@vtex/diagnostics-nodejs';
import { LogClient } from '@vtex/diagnostics-nodejs/dist/types';
import { getTelemetryClient } from '../telemetry';
import { Types } from '@vtex/diagnostics-nodejs';
import { initializeTelemetry } from '../telemetry';

let logClient: LogClient | undefined;
let client: Types.LogClient | undefined;
let isInitializing = false;
let initPromise: Promise<LogClient> | undefined = undefined;
let initPromise: Promise<Types.LogClient> | undefined = undefined;

export async function getLogClient(account: string, workspace: string, appName: string): Promise<LogClient> {
export async function getLogClient(): Promise<Types.LogClient> {

if (logClient) {
return logClient;
if (client) {
return client;
}

if (initPromise) {
return initPromise;
}

isInitializing = true;
initPromise = initializeClient(account, workspace, appName);
initPromise = initializeClient();

return initPromise;
}

async function initializeClient(account: string, workspace: string, appName: string): Promise<LogClient> {
async function initializeClient(): Promise<Types.LogClient> {
try {
const telemetryClient = await getTelemetryClient();

const logsConfig = Exporters.CreateLogsExporterConfig({
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
path: process.env.OTEL_EXPORTER_OTLP_PATH || '/v1/logs',
protocol: 'http',
interval: 5,
timeoutSeconds: 5,
headers: { 'Content-Type': 'application/json' },
});

const logsExporter = Exporters.CreateExporter(logsConfig, 'otlp');
await logsExporter.initialize();

const clientKey = `${account}-${workspace}-${appName}`;
logClient = await telemetryClient.newLogsClient({
exporter: logsExporter,
loggerName: `node-vtex-api-${clientKey}`,
});

return logClient;
const { logsClient } = await initializeTelemetry();
client = logsClient;
initPromise = undefined;
return logsClient;
} catch (error) {
console.error('Failed to initialize logs client:', error);
initPromise = undefined;
throw error;
} finally {
isInitializing = false;
Expand Down
10 changes: 5 additions & 5 deletions src/service/logger/logger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { APP, LOG_CLIENT_INIT_TIMEOUT_MS } from '../../constants'
import { cleanError } from '../../utils/error'
import { cleanLog } from '../../utils/log'
import { LogClient } from '@vtex/diagnostics-nodejs/dist/types';
import { Types } from '@vtex/diagnostics-nodejs';
import { LoggerContext, LogLevel, TracingState } from './loggerTypes'
import { getLogClient } from './client'

Expand All @@ -15,8 +15,8 @@ export class Logger {
private requestId: string
private production: boolean
private tracingState?: TracingState
private logClient: LogClient | undefined = undefined
private clientInitPromise: Promise<LogClient | undefined> | undefined = undefined
private logClient: Types.LogClient | undefined = undefined
private clientInitPromise: Promise<Types.LogClient | undefined> | undefined = undefined

constructor(ctx: LoggerContext) {
this.account = ctx.account
Expand All @@ -35,7 +35,7 @@ export class Logger {
// this.initLogClient();
}

private initLogClient(): Promise<LogClient | undefined> {
private initLogClient(): Promise<Types.LogClient | undefined> {
if (this.clientInitPromise) {
return this.clientInitPromise;
}
Expand All @@ -47,7 +47,7 @@ export class Logger {
});

this.logClient = await Promise.race([
getLogClient(this.account, this.workspace, APP.NAME),
getLogClient(),
timeoutPromise
]);

Expand Down
35 changes: 35 additions & 0 deletions src/service/metrics/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Types } from "@vtex/diagnostics-nodejs";
import { initializeTelemetry } from '../telemetry';

let client: Types.MetricClient | undefined;
let isInitializing = false;
let initPromise: Promise<Types.MetricClient> | undefined = undefined;

export async function getMetricClient(): Promise<Types.MetricClient> {
if (client) {
return client;
}

if (initPromise) {
return initPromise;
}

isInitializing = true;
initPromise = initializeClient();

return initPromise;
}
async function initializeClient(): Promise<Types.MetricClient> {
try {
const { metricsClient } = await initializeTelemetry();
client = metricsClient;
initPromise = undefined;
return metricsClient;
} catch (error) {
console.error('Failed to initialize metrics client:', error);
initPromise = undefined;
throw error;
} finally {
isInitializing = false;
}
}
41 changes: 41 additions & 0 deletions src/service/metrics/instruments/hostMetrics.ts
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');
}
}
}
101 changes: 101 additions & 0 deletions src/service/metrics/metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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
}

let instruments: OtelRequestInstruments | undefined
let initializingPromise: Promise<OtelRequestInstruments> | undefined

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'
})
}

export const getOtelInstruments = async (): Promise<OtelRequestInstruments> => {
if (instruments) {
return instruments
}

if (initializingPromise) {
return initializingPromise
}

initializingPromise = initializeOtelInstruments()

try {
instruments = await initializingPromise
return instruments
} finally {
initializingPromise = undefined
}
}

const initializeOtelInstruments = async (): Promise<OtelRequestInstruments> => {
const [
concurrentRequests,
requestTimings,
totalRequests,
responseSizes,
abortedRequests
] = await Promise.all([
createOtelConcurrentRequestsInstrument(),
createOtelRequestsTimingsInstrument(),
createOtelTotalRequestsInstrument(),
createOtelRequestsResponseSizesInstrument(),
createOtelTotalAbortedRequestsInstrument()
])

return {
concurrentRequests,
requestTimings,
totalRequests,
responseSizes,
abortedRequests
}
}
Loading
Loading