diff --git a/index.js b/index.js index f5fa36c2e2..f1d1d18c3f 100644 --- a/index.js +++ b/index.js @@ -7,4 +7,9 @@ require('werelogs').stderrUtils.catchAndTimestampStderr( require('cluster').isPrimary ? 1 : null, ); +// Start tracing before requiring anything that hooks into HTTP, MongoDB, +// or ioredis — instrumentation patches modules on require, so anything +// loaded earlier than init() would run unpatched. +require('./lib/tracing').init(); + require('./lib/server.js')(); diff --git a/lib/api/api.js b/lib/api/api.js index 0f6d39f5ef..d1408f039a 100644 --- a/lib/api/api.js +++ b/lib/api/api.js @@ -69,8 +69,7 @@ const objectPutPart = require('./objectPutPart'); const objectPutCopyPart = require('./objectPutCopyPart'); const objectPutRetention = require('./objectPutRetention'); const objectRestore = require('./objectRestore'); -const prepareRequestContexts - = require('./apiUtils/authorization/prepareRequestContexts'); +const prepareRequestContexts = require('./apiUtils/authorization/prepareRequestContexts'); const serviceGet = require('./serviceGet'); const vault = require('../auth/vault'); const website = require('./website'); @@ -80,6 +79,7 @@ const parseCopySource = require('./apiUtils/object/parseCopySource'); const { tagConditionKeyAuth } = require('./apiUtils/authorization/tagConditionKeys'); const { isRequesterASessionUser } = require('./apiUtils/authorization/permissionChecks'); const checkHttpHeadersSize = require('./apiUtils/object/checkHttpHeadersSize'); +const { instrumentApiMethod } = require('../instrumentation/simple'); const constants = require('../../constants'); const { config } = require('../Config.js'); const metadata = require('../metadata/wrapper'); @@ -102,8 +102,7 @@ auth.setHandler(vault); // scan the args instead of relying on a fixed position. function hasCorsHeaders(callbackArgs) { for (const arg of callbackArgs) { - if (!arg || typeof arg !== 'object' || Array.isArray(arg) - || Buffer.isBuffer(arg)) { + if (!arg || typeof arg !== 'object' || Array.isArray(arg) || Buffer.isBuffer(arg)) { continue; } if ('access-control-allow-origin' in arg) { @@ -137,22 +136,21 @@ function wrapCallbackWithErrorCorsHeaders(callback, request, response, log) { if (!bucket || response.headersSent) { return; } - const headers = collectCorsHeaders( - request.headers.origin, request.method, bucket); + const headers = collectCorsHeaders(request.headers.origin, request.method, bucket); Object.keys(headers).forEach(key => { try { response.setHeader(key, headers[key]); } catch (e) { log.debug('could not set CORS header on error', { - header: key, error: e.message, + header: key, + error: e.message, }); } }); } return (err, ...callbackArgs) => { - if (!err || !request.headers || !request.headers.origin - || !request.bucketName) { + if (!err || !request.headers || !request.headers.origin || !request.bucketName) { return callback(err, ...callbackArgs); } // Fast path: most post-auth failures come back with corsHeaders @@ -194,8 +192,7 @@ function checkAuthResults(authResults, apiMethod, log) { isImplicitDeny[authResults[0].action] = authResults[0].isImplicit; // second item checks s3:GetObject(Version)Tagging action if (!authResults[1].isAllowed) { - log.trace('get tagging authorization denial ' + - 'from Vault'); + log.trace('get tagging authorization denial ' + 'from Vault'); returnTagCount = false; } } else { @@ -256,14 +253,15 @@ function callApiHandler(apiMethod, apiHandler, request, response, log, callback) } // no need to check auth on website or cors preflight requests - if (apiMethod === 'websiteGet' || apiMethod === 'websiteHead' || - apiMethod === 'corsPreflight') { + if (apiMethod === 'websiteGet' || apiMethod === 'websiteHead' || apiMethod === 'corsPreflight') { request.actionImplicitDenies = false; return apiHandler(request, log, callback); } - const { sourceBucket, sourceObject, sourceVersionId, parsingError } = - parseCopySource(apiMethod, request.headers['x-amz-copy-source']); + const { sourceBucket, sourceObject, sourceVersionId, parsingError } = parseCopySource( + apiMethod, + request.headers['x-amz-copy-source'], + ); if (parsingError) { log.debug('error parsing copy source', { error: parsingError, @@ -279,172 +277,185 @@ function callApiHandler(apiMethod, apiHandler, request, response, log, callback) return process.nextTick(callback, httpHeadersSizeError); } - const requestContexts = prepareRequestContexts(apiMethod, request, - sourceBucket, sourceObject, sourceVersionId); + const requestContexts = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId); // Extract all the _apiMethods and store them in an array const apiMethods = requestContexts ? requestContexts.map(context => context._apiMethod) : []; // Attach the names to the current request request.apiMethods = apiMethods; - return async.waterfall([ - next => auth.server.doAuth( - request, log, (err, userInfo, authorizationResults, streamingV4Params, infos) => { - if (request.serverAccessLog) { - request.serverAccessLog.authInfo = userInfo; + return async.waterfall( + [ + next => + auth.server.doAuth( + request, + log, + (err, userInfo, authorizationResults, streamingV4Params, infos) => { + if (request.serverAccessLog) { + request.serverAccessLog.authInfo = userInfo; + } + if (err) { + // VaultClient returns standard errors, but the route requires + // Arsenal errors + const arsenalError = err.metadata ? err : errors[err.code] || errors.InternalError; + log.trace('authentication error', { error: err }); + return next(arsenalError); + } + return next(null, userInfo, authorizationResults, streamingV4Params, infos); + }, + 's3', + requestContexts, + ), + (userInfo, authorizationResults, streamingV4Params, infos, next) => { + const authNames = { accountName: userInfo.getAccountDisplayName() }; + if (userInfo.isRequesterAnIAMUser()) { + authNames.userName = userInfo.getIAMdisplayName(); } - if (err) { - // VaultClient returns standard errors, but the route requires - // Arsenal errors - const arsenalError = err.metadata ? err : errors[err.code] || errors.InternalError; - log.trace('authentication error', { error: err }); - return next(arsenalError); + if (isRequesterASessionUser(userInfo)) { + authNames.sessionName = userInfo.getShortid().split(':')[1]; } - return next(null, userInfo, authorizationResults, streamingV4Params, infos); - }, 's3', requestContexts), - (userInfo, authorizationResults, streamingV4Params, infos, next) => { - const authNames = { accountName: userInfo.getAccountDisplayName() }; - if (userInfo.isRequesterAnIAMUser()) { - authNames.userName = userInfo.getIAMdisplayName(); - } - if (isRequesterASessionUser(userInfo)) { - authNames.sessionName = userInfo.getShortid().split(':')[1]; - } - log.addDefaultFields(authNames); - if (request.serverAccessLog) { - request.serverAccessLog.analyticsAccountName = authNames.accountName; - request.serverAccessLog.analyticsUserName = authNames.userName; - } - if (apiMethod === 'objectPut' || apiMethod === 'objectPutPart') { - const setStartTurnAroundTime = () => { - if (request.serverAccessLog) { - request.serverAccessLog.startTurnAroundTime = process.hrtime.bigint(); - } - }; - // For 0-byte uploads, downstream handlers do not consume - // the request stream, so 'end' never fires. Set - // startTurnAroundTime synchronously in that case. - if (request.headers['content-length'] === '0') { - setStartTurnAroundTime(); - } else { - request.on('end', setStartTurnAroundTime); + log.addDefaultFields(authNames); + if (request.serverAccessLog) { + request.serverAccessLog.analyticsAccountName = authNames.accountName; + request.serverAccessLog.analyticsUserName = authNames.userName; } - return next(null, userInfo, authorizationResults, streamingV4Params, infos); - } - // issue 100 Continue to the client - writeContinue(request, response); - - const defaultMaxBodyLength = request.method === 'POST' ? - constants.oneMegaBytes : constants.halfMegaBytes; - const MAX_BODY_LENGTH = config.apiBodySizeLimits[apiMethod] || defaultMaxBodyLength; - const post = []; - let bodyLength = 0; - request.on('data', chunk => { - bodyLength += chunk.length; - // Sanity check on post length - if (bodyLength <= MAX_BODY_LENGTH) { - post.push(chunk); + if (apiMethod === 'objectPut' || apiMethod === 'objectPutPart') { + const setStartTurnAroundTime = () => { + if (request.serverAccessLog) { + request.serverAccessLog.startTurnAroundTime = process.hrtime.bigint(); + } + }; + // For 0-byte uploads, downstream handlers do not consume + // the request stream, so 'end' never fires. Set + // startTurnAroundTime synchronously in that case. + if (request.headers['content-length'] === '0') { + setStartTurnAroundTime(); + } else { + request.on('end', setStartTurnAroundTime); + } + return next(null, userInfo, authorizationResults, streamingV4Params, infos); } - }); + // issue 100 Continue to the client + writeContinue(request, response); + + const defaultMaxBodyLength = + request.method === 'POST' ? constants.oneMegaBytes : constants.halfMegaBytes; + const MAX_BODY_LENGTH = config.apiBodySizeLimits[apiMethod] || defaultMaxBodyLength; + const post = []; + let bodyLength = 0; + request.on('data', chunk => { + bodyLength += chunk.length; + // Sanity check on post length + if (bodyLength <= MAX_BODY_LENGTH) { + post.push(chunk); + } + }); - request.on('error', err => { - log.trace('error receiving request', { - error: err, + request.on('error', err => { + log.trace('error receiving request', { + error: err, + }); + return next(errors.InternalError); }); - return next(errors.InternalError); - }); - request.on('end', () => { - if (request.serverAccessLog) { - request.serverAccessLog.startTurnAroundTime = process.hrtime.bigint(); - } + request.on('end', () => { + if (request.serverAccessLog) { + request.serverAccessLog.startTurnAroundTime = process.hrtime.bigint(); + } - if (bodyLength > MAX_BODY_LENGTH) { - log.error('body length is too long for request type', - { bodyLength }); - return next(errors.InvalidRequest); - } + if (bodyLength > MAX_BODY_LENGTH) { + log.error('body length is too long for request type', { bodyLength }); + return next(errors.InvalidRequest); + } - const buff = Buffer.concat(post, bodyLength); + const buff = Buffer.concat(post, bodyLength); - const err = validateMethodChecksumNoChunking(request, buff, log); - if (err) { - return next(err); - } + const err = validateMethodChecksumNoChunking(request, buff, log); + if (err) { + return next(err); + } - // Convert array of post buffers into one string - request.post = buff.toString(); - return next(null, userInfo, authorizationResults, streamingV4Params, infos); - }); - return undefined; - }, - // Tag condition keys require information from CloudServer for evaluation - (userInfo, authorizationResults, streamingV4Params, infos, next) => tagConditionKeyAuth( - authorizationResults, - request, - requestContexts, - apiMethod, - log, - (err, authResultsWithTags) => { - if (err) { - log.trace('tag authentication error', { error: err }); - return next(err); - } - return next(null, userInfo, authResultsWithTags, streamingV4Params, infos); + // Convert array of post buffers into one string + request.post = buff.toString(); + return next(null, userInfo, authorizationResults, streamingV4Params, infos); + }); + return undefined; }, - ), - (userInfo, authorizationResults, streamingV4Params, infos, next) => handleAuthorizationResults( - request, authorizationResults, apiMethod, returnTagCount, log, (err, res) => { - request.accountQuotas = infos?.accountQuota; - request.accountLimits = infos?.limits; - if (err) { - return next(err); - } - returnTagCount = res.returnTagCount; - return next(null, userInfo, authorizationResults, streamingV4Params); - }), - ], (err, userInfo, authorizationResults, streamingV4Params) => { - if (err) { - return callback(err); - } - const methodCallback = (err, ...results) => async.forEachLimit(request.finalizerHooks, 5, - (hook, done) => hook(err, done), - () => callback(err, ...results)); - - if (apiMethod === 'objectPut' || apiMethod === 'objectPutPart') { - request._response = response; - return apiHandler(userInfo, request, streamingV4Params, - log, methodCallback, authorizationResults); - } - if (apiMethod === 'objectCopy' || apiMethod === 'objectPutCopyPart') { - return apiHandler(userInfo, request, sourceBucket, - sourceObject, sourceVersionId, log, methodCallback); - } - if (apiMethod === 'objectGet') { - // remove objectGetTagging/objectGetTaggingVersion from apiMethods, these were added by - // prepareRequestContexts to determine the value of returnTagCount. - request.apiMethods = request.apiMethods.filter(methodName => !methodName.includes('Tagging')); - return apiHandler(userInfo, request, returnTagCount, log, callback); - } - return apiHandler(userInfo, request, log, methodCallback); - }); + // Tag condition keys require information from CloudServer for evaluation + (userInfo, authorizationResults, streamingV4Params, infos, next) => + tagConditionKeyAuth( + authorizationResults, + request, + requestContexts, + apiMethod, + log, + (err, authResultsWithTags) => { + if (err) { + log.trace('tag authentication error', { error: err }); + return next(err); + } + return next(null, userInfo, authResultsWithTags, streamingV4Params, infos); + }, + ), + (userInfo, authorizationResults, streamingV4Params, infos, next) => + handleAuthorizationResults( + request, + authorizationResults, + apiMethod, + returnTagCount, + log, + (err, res) => { + request.accountQuotas = infos?.accountQuota; + request.accountLimits = infos?.limits; + if (err) { + return next(err); + } + returnTagCount = res.returnTagCount; + return next(null, userInfo, authorizationResults, streamingV4Params); + }, + ), + ], + (err, userInfo, authorizationResults, streamingV4Params) => { + if (err) { + return callback(err); + } + const methodCallback = (err, ...results) => + async.forEachLimit( + request.finalizerHooks, + 5, + (hook, done) => hook(err, done), + () => callback(err, ...results), + ); + + if (apiMethod === 'objectPut' || apiMethod === 'objectPutPart') { + request._response = response; + return apiHandler(userInfo, request, streamingV4Params, log, methodCallback, authorizationResults); + } + if (apiMethod === 'objectCopy' || apiMethod === 'objectPutCopyPart') { + return apiHandler(userInfo, request, sourceBucket, sourceObject, sourceVersionId, log, methodCallback); + } + if (apiMethod === 'objectGet') { + // remove objectGetTagging/objectGetTaggingVersion from apiMethods, these were added by + // prepareRequestContexts to determine the value of returnTagCount. + request.apiMethods = request.apiMethods.filter(methodName => !methodName.includes('Tagging')); + return apiHandler(userInfo, request, returnTagCount, log, callback); + } + return apiHandler(userInfo, request, log, methodCallback); + }, + ); } const api = { callApiMethod(apiMethod, request, response, log, callback) { // Attach the apiMethod method to the request, so it can used by monitoring in the server request.apiMethod = apiMethod; - callback = wrapCallbackWithErrorCorsHeaders( - callback, request, response, log); + callback = wrapCallbackWithErrorCorsHeaders(callback, request, response, log); // Array of end of API callbacks, used to perform some logic // at the end of an API. request.finalizerHooks = []; const actionLog = monitoringMap[apiMethod]; - if (!actionLog && - apiMethod !== 'websiteGet' && - apiMethod !== 'websiteHead' && - apiMethod !== 'corsPreflight') { + if (!actionLog && apiMethod !== 'websiteGet' && apiMethod !== 'websiteHead' && apiMethod !== 'corsPreflight') { log.error('callApiMethod(): No actionLog for this api method', { apiMethod, }); @@ -586,4 +597,14 @@ const api = { handleAuthorizationResults, }; +// Denylist (not allowlist) so newly-added S3 handlers are auto-traced +// without a separate registration step. The three skipped keys are +// internal helpers, not S3 operations. +const NON_INSTRUMENTED_KEYS = new Set(['callApiMethod', 'checkAuthResults', 'handleAuthorizationResults']); +for (const [name, handler] of Object.entries(api)) { + if (typeof handler === 'function' && !NON_INSTRUMENTED_KEYS.has(name)) { + api[name] = instrumentApiMethod(handler, name); + } +} + module.exports = api; diff --git a/lib/instrumentation/simple.js b/lib/instrumentation/simple.js new file mode 100644 index 0000000000..55f46ba6c6 --- /dev/null +++ b/lib/instrumentation/simple.js @@ -0,0 +1,92 @@ +'use strict'; + +const tracing = require('../tracing'); + +let tracer = null; +function getTracer() { + if (tracer) { + return tracer; + } + const { trace } = require('@opentelemetry/api'); + const { version } = require('../../package.json'); + tracer = trace.getTracer('cloudserver-api', version); + return tracer; +} + +async function endSpanWhenSettled(promise, endSpan) { + try { + const value = await promise; + endSpan(); + return value; + } catch (err) { + endSpan(err); + throw err; + } +} + +function instrumentApiMethod(apiMethod, methodName) { + if (!tracing.isEnabled()) { + return apiMethod; + } + + const api = require('@opentelemetry/api'); + const spanName = `api.${methodName}`; + + return function instrumented(...args) { + const callbackIndex = args.findLastIndex(a => typeof a === 'function'); + const span = getTracer().startSpan(spanName, { kind: api.SpanKind.INTERNAL }); + + // End-once guard. Multiple termination paths can race: the + // wrapped callback may fire and then the handler may also throw + // synchronously, or a callback-and-Promise hybrid handler may + // resolve after firing the callback. + let spanEnded = false; + const endSpan = err => { + if (spanEnded) { + return; + } + spanEnded = true; + if (err) { + span.recordException(err); + span.setStatus({ code: api.SpanStatusCode.ERROR }); + if (err.code) { + span.setAttribute('cloudserver.error_code', err.code); + } + } else { + span.setStatus({ code: api.SpanStatusCode.OK }); + } + span.end(); + }; + + const wrappedArgs = [...args]; + if (callbackIndex !== -1) { + const originalCallback = args[callbackIndex]; + wrappedArgs[callbackIndex] = function wrappedCallback(err, ...results) { + endSpan(err); + return originalCallback.call(this, err, ...results); + }; + } + + const ctx = api.trace.setSpan(api.context.active(), span); + try { + const result = api.context.with(ctx, () => apiMethod.apply(this, wrappedArgs)); + if (callbackIndex === -1) { + if (result && typeof result.then === 'function') { + return endSpanWhenSettled(result, endSpan); + } + endSpan(); + } + // Callback-style handler: the wrapped callback drives the + // span lifecycle. If the handler also returns a thenable + // (hybrid migration shape), pass it through untouched — + // attaching a second .then() chain would surface as an + // unhandled rejection in callback-only callers. + return result; + } catch (error) { + endSpan(error); + throw error; + } + }; +} + +module.exports = { instrumentApiMethod }; diff --git a/lib/server.js b/lib/server.js index a5bb364b61..1946633950 100644 --- a/lib/server.js +++ b/lib/server.js @@ -6,6 +6,7 @@ const arsenal = require('arsenal'); const { setServerHeader } = arsenal.s3routes.routesUtils; const { RedisClient, StatsClient } = arsenal.metrics; const monitoringClient = require('./utilities/monitoringHandler'); +const tracing = require('./tracing'); const logger = require('./utilities/logger'); const { internalHandlers } = require('./utilities/internalHandlers'); @@ -15,15 +16,11 @@ const { blacklistedPrefixes } = require('../constants'); const api = require('./api/api'); const dataWrapper = require('./data/wrapper'); const kms = require('./kms/wrapper'); -const locationStorageCheck = - require('./api/apiUtils/object/locationStorageCheck'); +const locationStorageCheck = require('./api/apiUtils/object/locationStorageCheck'); const vault = require('./auth/vault'); const metadata = require('./metadata/wrapper'); const { initManagement } = require('./management'); -const { - initManagementClient, - isManagementAgentUsed, -} = require('./management/agentClient'); +const { initManagementClient, isManagementAgentUsed } = require('./management/agentClient'); const { startCleanupJob } = require('./api/apiUtils/rateLimit/cleanup'); const { startRefillJob, stopRefillJob } = require('./api/apiUtils/rateLimit/refillJob'); @@ -46,8 +43,7 @@ updateAllEndpoints(); _config.on('location-constraints-update', () => { if (implName === 'multipleBackends') { const clients = parseLC(_config, vault); - client = new MultipleBackendGateway( - clients, metadata, locationStorageCheck); + client = new MultipleBackendGateway(clients, metadata, locationStorageCheck); } }); @@ -59,8 +55,7 @@ if (_config.localCache) { // stats client const STATS_INTERVAL = 5; // 5 seconds const STATS_EXPIRY = 30; // 30 seconds -const statsClient = new StatsClient(localCacheClient, STATS_INTERVAL, - STATS_EXPIRY); +const statsClient = new StatsClient(localCacheClient, STATS_INTERVAL, STATS_EXPIRY); const enableRemoteManagement = true; class S3Server { @@ -84,7 +79,7 @@ class S3Server { process.on('SIGHUP', this.cleanUp.bind(this)); process.on('SIGQUIT', this.cleanUp.bind(this)); process.on('SIGTERM', this.cleanUp.bind(this)); - process.on('SIGPIPE', () => { }); + process.on('SIGPIPE', () => {}); // This will pick up exceptions up the stack process.on('uncaughtException', err => { // If just send the error object results in empty @@ -130,9 +125,10 @@ class S3Server { const requestStartTime = process.hrtime.bigint(); // Skip server access logs for heartbeat. - const isLoggingEnabled = _config.serverAccessLogs - && (_config.serverAccessLogs.mode === serverAccessLogsModes.LOG_ONLY - || _config.serverAccessLogs.mode === serverAccessLogsModes.ENABLED); + const isLoggingEnabled = + _config.serverAccessLogs && + (_config.serverAccessLogs.mode === serverAccessLogsModes.LOG_ONLY || + _config.serverAccessLogs.mode === serverAccessLogsModes.ENABLED); const isInternalRoute = req.url.startsWith('/_'); const isBackbeatRoute = req.url.startsWith('/_/backbeat/'); if (isLoggingEnabled && (!isInternalRoute || isBackbeatRoute)) { @@ -176,9 +172,7 @@ class S3Server { labels.action = req.apiMethod; } monitoringClient.httpRequestsTotal.labels(labels).inc(); - monitoringClient.httpRequestDurationSeconds - .labels(labels) - .observe(responseTimeInNs / 1e9); + monitoringClient.httpRequestDurationSeconds.labels(labels).observe(responseTimeInNs / 1e9); monitoringClient.httpActiveRequests.dec(); }; res.on('close', monitorEndOfRequest); @@ -206,6 +200,7 @@ class S3Server { vault, }, }; + arsenal.s3routes.routes(req, res, params, logger, this.config); } @@ -231,14 +226,13 @@ class S3Server { }; let reqUids = req.headers['x-scal-request-uids']; - if (reqUids !== undefined && !/*isValidReqUids*/(reqUids.length < 128)) { + if (reqUids !== undefined && !(/*isValidReqUids*/ (reqUids.length < 128))) { // simply ignore invalid id (any user can provide an // invalid request ID through a crafted header) reqUids = undefined; } - const log = (reqUids !== undefined ? - logger.newRequestLoggerFromSerializedUids(reqUids) : - logger.newRequestLogger()); + const log = + reqUids !== undefined ? logger.newRequestLoggerFromSerializedUids(reqUids) : logger.newRequestLogger(); log.end().addDefaultFields(clientInfo); log.debug('received admin request', clientInfo); @@ -292,8 +286,7 @@ class S3Server { server.requestTimeout = 0; // disabling request timeout server.on('connection', socket => { - socket.on('error', err => logger.info('request rejected', - { error: err })); + socket.on('error', err => logger.info('request rejected', { error: err })); }); // https://nodejs.org/dist/latest-v6.x/ @@ -309,8 +302,11 @@ class S3Server { }; const { address } = addr; logger.info('server started', { - address, port, - pid: process.pid, serverIP: address, serverPort: port + address, + port, + pid: process.pid, + serverIP: address, + serverPort: port, }); }); @@ -323,32 +319,41 @@ class S3Server { this.servers.push(server); } - /* - * This exits the running process properly. - */ - cleanUp() { + async cleanUp() { logger.info('server shutting down'); - // Stop token refill job if running if (this.config.rateLimiting?.enabled) { stopRefillJob(logger); } - Promise.all(this.servers.map(server => - new Promise(resolve => server.close(resolve)) - )).then(() => process.exit(0)); + try { + await Promise.all(this.servers.map(server => new Promise(resolve => server.close(resolve)))); + await tracing.close(); + } finally { + process.exit(0); + } } - caughtExceptionShutdown() { + async caughtExceptionShutdown() { if (!this.cluster) { - process.exit(1); + try { + await tracing.close(); + } finally { + process.exit(1); + } + return; } logger.error('shutdown of worker due to exception', { workerId: this.worker ? this.worker.id : undefined, workerPid: this.worker ? this.worker.process.pid : undefined, }); - // Will close all servers, cause disconnect event on primary and kill - // worker process with 'SIGTERM'. + // worker.kill() is graceful (closes servers, disconnects IPC) but + // does not fire our SIGTERM handler, so the BatchSpanProcessor + // would lose buffered spans without an explicit flush here. if (this.worker) { - this.worker.kill(); + try { + await tracing.close(); + } finally { + this.worker.kill(); + } } } @@ -363,10 +368,7 @@ class S3Server { } initiateStartup(log) { - series([ - next => metadata.setup(next), - next => clientCheck(true, log, next), - ], (err, results) => { + series([next => metadata.setup(next), next => clientCheck(true, log, next)], (err, results) => { if (err) { log.warn('initial health check failed, delaying startup', { error: err, @@ -417,8 +419,10 @@ class S3Server { try { logger.info('ServerAccessLogger config', { config: _config.serverAccessLogs }); - if (_config.serverAccessLogs.mode === serverAccessLogsModes.LOG_ONLY - || _config.serverAccessLogs.mode === serverAccessLogsModes.ENABLED) { + if ( + _config.serverAccessLogs.mode === serverAccessLogsModes.LOG_ONLY || + _config.serverAccessLogs.mode === serverAccessLogsModes.ENABLED + ) { var serverAccessLogger = new ServerAccessLogger( _config.serverAccessLogs.outputFile, _config.serverAccessLogs.highWaterMarkBytes, @@ -434,7 +438,6 @@ class S3Server { logger.error('ServerAccessLogger creation error', error); } - this.started = true; }); } @@ -490,8 +493,7 @@ function main() { }); const metricServer = new S3Server(_config); - metricServer.startServer(_config.metricsListenOn, - _config.metricsPort, metricServer.routeAdminRequest); + metricServer.startServer(_config.metricsListenOn, _config.metricsPort, metricServer.routeAdminRequest); } if (_config.isCluster && cluster.isWorker) { const server = new S3Server(_config, cluster.worker); diff --git a/lib/tracing/healthPaths.js b/lib/tracing/healthPaths.js new file mode 100644 index 0000000000..340c58c5ad --- /dev/null +++ b/lib/tracing/healthPaths.js @@ -0,0 +1,18 @@ +'use strict'; + +// Probe + scrape paths that should never produce a span. Filtered at +// ingest (not at the trace backend) because probe rate × pod count × +// always-on sampling overwhelms the exporter and storage with traffic +// nobody queries. +const HEALTH_PATHS = new Set(['/live', '/ready', '/_/healthcheck', '/_/healthcheck/deep', '/metrics']); + +function isHealthPath(url) { + if (typeof url !== 'string' || url.length === 0) { + return false; + } + const qIdx = url.indexOf('?'); + const path = qIdx === -1 ? url : url.slice(0, qIdx); + return HEALTH_PATHS.has(path); +} + +module.exports = { isHealthPath }; diff --git a/lib/tracing/index.js b/lib/tracing/index.js new file mode 100644 index 0000000000..abd7ce0315 --- /dev/null +++ b/lib/tracing/index.js @@ -0,0 +1,108 @@ +'use strict'; + +const { buildTrustedHosts, makeRequestHook } = require('./trustedHosts'); +const { isHealthPath } = require('./healthPaths'); + +let sdk = null; + +function isEnabled() { + return process.env.ENABLE_OTEL === 'true'; +} + +function init() { + if (!isEnabled() || sdk) { + return; + } + + const endpoint = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT; + if (!endpoint) { + throw new Error('ENABLE_OTEL=true but OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is unset'); + } + + const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api'); + const { NodeSDK } = require('@opentelemetry/sdk-node'); + const { resourceFromAttributes } = require('@opentelemetry/resources'); + const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); + const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); + const { IORedisInstrumentation } = require('@opentelemetry/instrumentation-ioredis'); + const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongodb'); + const { ParentBasedSampler, TraceIdRatioBasedSampler } = require('@opentelemetry/sdk-trace-base'); + const { version } = require('../../package.json'); + const { config } = require('../Config'); + + diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.WARN); + + const parsedRatio = parseFloat(process.env.OTEL_SAMPLING_RATIO); + const samplingRatio = Number.isFinite(parsedRatio) ? parsedRatio : 0.01; + + const trustedHosts = buildTrustedHosts(config); + + const ignoreIncomingRequestHook = req => req.method === 'OPTIONS' || isHealthPath(req.url); + + sdk = new NodeSDK({ + resource: resourceFromAttributes({ + 'service.name': process.env.OTEL_SERVICE_NAME || 'cloudserver', + 'service.version': process.env.OTEL_SERVICE_VERSION || version, + 'service.namespace': process.env.OTEL_SERVICE_NAMESPACE || 'scality', + }), + traceExporter: new OTLPTraceExporter({ url: endpoint }), + logRecordProcessors: [], + metricReaders: [], + spanLimits: { + attributeValueLengthLimit: 4096, + attributeCountLimit: 128, + eventCountLimit: 128, + linkCountLimit: 128, + }, + sampler: new ParentBasedSampler({ + root: new TraceIdRatioBasedSampler(samplingRatio), + }), + instrumentations: [ + new HttpInstrumentation({ + ignoreIncomingRequestHook, + requestHook: makeRequestHook(trustedHosts), + }), + new IORedisInstrumentation({ requireParentSpan: true }), + // Mask leaf values in db.statement so query shape is captured + // without user data (object keys, filter values) flowing to + // the trace backend. + new MongoDBInstrumentation({ enhancedDatabaseReporting: false }), + ], + }); + + sdk.start(); +} + +// Cap the flush window. The BatchSpanProcessor's default 30s export +// timeout would otherwise block process.exit, and Kubernetes' default +// terminationGracePeriodSeconds is also 30s — we'd get SIGKILL'd +// before the flush ever completed. +const SHUTDOWN_DEADLINE_MS = 5000; + +async function close() { + // Capture + clear before awaiting so concurrent callers (SIGTERM + // during an uncaught-exception flow, for example) don't both call + // sdk.shutdown() — the SDK doesn't guarantee idempotent shutdown. + const local = sdk; + if (!local) { + return; + } + sdk = null; + try { + await Promise.race([ + local.shutdown(), + // .unref() so the timer doesn't pin the event loop open + // when sdk.shutdown() resolves first. + new Promise(resolve => { + setTimeout(resolve, SHUTDOWN_DEADLINE_MS).unref(); + }), + ]); + } catch (err) { + // Loggers may already be torn down at this point in shutdown; + // log to stderr directly. + // eslint-disable-next-line no-console + console.error('tracing close failed', err); + } +} + +module.exports = { init, close, isEnabled }; diff --git a/lib/tracing/trustedHosts.js b/lib/tracing/trustedHosts.js new file mode 100644 index 0000000000..870cdb1f07 --- /dev/null +++ b/lib/tracing/trustedHosts.js @@ -0,0 +1,100 @@ +'use strict'; + +function extractHost(s) { + if (typeof s !== 'string' || s.length === 0) { + return undefined; + } + if (s.includes('://')) { + try { + return new URL(s).hostname.toLowerCase(); + } catch { + // fall through to plain host:port parsing + } + } + return s.split(':')[0].toLowerCase(); +} + +// On outbound requests to hosts outside `trustedHosts`, strip +// traceparent/tracestate and tag the client span as suppressed — +// preserve the span (we still want to observe the call) without +// leaking trace IDs to external destinations. +function makeRequestHook(trustedHosts) { + return function requestHook(span, request) { + // IncomingMessage (inbound server spans) doesn't expose + // getHeader/removeHeader; only ClientRequest does. + if (!request || typeof request.getHeader !== 'function') { + return; + } + const host = extractHost((request.getHeader('host') || '').toString()); + if (trustedHosts.has(host)) { + return; + } + if (typeof request.removeHeader === 'function') { + request.removeHeader('traceparent'); + request.removeHeader('tracestate'); + } + if (span && typeof span.setAttribute === 'function') { + span.setAttribute('scality.trace.suppressed', true); + } + }; +} + +// Derived from cloudserver's Config so it stays honest as new backends +// land. A unit test asserts the set against a fixture Config. +function buildTrustedHosts(config) { + const hosts = new Set(['localhost', '127.0.0.1', '::1']); + + const add = v => { + const h = extractHost(v); + if (h) { + hosts.add(h); + } + }; + + if (!config) { + return hosts; + } + + add(config.vaultd?.host); + add(config.dataClient?.host); + add(config.metadataClient?.host); + add(config.pfsClient?.host); + add(config.cdmi?.host); + add(config.scuba?.host); + add(config.utapi?.host); + add(config.localCache?.host); + add(config.managementAgent?.host); + add(config.backbeat?.host); + add(config.kmsAWS?.endpoint); + + config.bucketd?.bootstrap?.forEach(add); + + if (config.kmip?.transport) { + const transports = Array.isArray(config.kmip.transport) ? config.kmip.transport : [config.kmip.transport]; + transports.forEach(t => add(t?.tls?.host)); + } + + if (typeof config.mongodb?.replicaSetHosts === 'string') { + config.mongodb.replicaSetHosts.split(',').forEach(add); + } + + // Read directly from env; lib/management/index.js sources these + // there too, they don't flow through Config.js. + add(process.env.PUSH_ENDPOINT); + add(process.env.MANAGEMENT_ENDPOINT); + + // Only the two Scality-owned connector shapes are trusted. Every + // other locationType (aws_s3, azure, gcp, *-archive, dmf, file, ...) + // is a separate cluster or external cloud — those stay untrusted + // so trace context doesn't leak across cluster boundaries. + if (config.locationConstraints && typeof config.locationConstraints === 'object') { + for (const loc of Object.values(config.locationConstraints)) { + loc?.details?.connector?.hdclient?.bootstrap?.forEach(add); + loc?.details?.connector?.sproxyd?.bootstrap?.forEach(add); + } + } + + return hosts; +} + +module.exports = { extractHost, buildTrustedHosts, makeRequestHook }; diff --git a/package.json b/package.json index 8bbc285057..f909011df6 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,16 @@ "@aws-sdk/signature-v4": "^3.374.0", "@azure/storage-blob": "^12.28.0", "@hapi/joi": "^17.1.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-trace-otlp-http": "~0.218.0", + "@opentelemetry/instrumentation-http": "~0.218.0", + "@opentelemetry/instrumentation-ioredis": "~0.64.0", + "@opentelemetry/instrumentation-mongodb": "~0.69.0", + "@opentelemetry/resources": "^2.7.0", + "@opentelemetry/sdk-node": "~0.218.0", + "@opentelemetry/sdk-trace-base": "^2.7.0", "@smithy/node-http-handler": "^3.0.0", - "arsenal": "git+https://github.com/scality/Arsenal#8.4.1", + "arsenal": "git+https://github.com/scality/Arsenal#improvement/ARSN-572/trace-context", "async": "2.6.4", "bucketclient": "scality/bucketclient#8.2.7", "bufferutil": "^4.0.8", @@ -91,7 +99,8 @@ "nan": "v2.22.0", "fast-xml-parser": "^5.5.6", "ts-morph/**/brace-expansion": "^5.0.5", - "ts-morph/**/picomatch": "^4.0.4" + "ts-morph/**/picomatch": "^4.0.4", + "@opentelemetry/api": "^1.9.0" }, "countAsyncSourcePaths": [ "lib/**/*.js", diff --git a/tests/unit/lib/instrumentationSimple.spec.js b/tests/unit/lib/instrumentationSimple.spec.js new file mode 100644 index 0000000000..9c023918a5 --- /dev/null +++ b/tests/unit/lib/instrumentationSimple.spec.js @@ -0,0 +1,192 @@ +'use strict'; + +// Force the module under test onto its OTEL-on path. Must be set before +// any require pulls in lib/instrumentation/simple. +process.env.ENABLE_OTEL = 'true'; + +const assert = require('assert'); +const { trace, SpanStatusCode } = require('@opentelemetry/api'); +const { + BasicTracerProvider, + InMemorySpanExporter, + SimpleSpanProcessor, + AlwaysOnSampler, +} = require('@opentelemetry/sdk-trace-base'); + +const exporter = new InMemorySpanExporter(); +const provider = new BasicTracerProvider({ + sampler: new AlwaysOnSampler(), + spanProcessors: [new SimpleSpanProcessor(exporter)], +}); +trace.setGlobalTracerProvider(provider); + +const { instrumentApiMethod } = require('../../../lib/instrumentation/simple'); + +describe('instrumentApiMethod', () => { + describe('OTEL on', () => { + afterEach(() => exporter.reset()); + + it('wraps a callback handler and ends span on success', done => { + const handler = (a, b, cb) => cb(null, 'ok'); + const wrapped = instrumentApiMethod(handler, 'objectGet'); + + wrapped('foo', 'bar', (err, value) => { + assert.strictEqual(err, null); + assert.strictEqual(value, 'ok'); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, 'api.objectGet'); + assert.strictEqual(spans[0].status.code, SpanStatusCode.OK); + done(); + }); + }); + + it("ends span with ERROR when handler's callback fires with err", done => { + const handler = (a, cb) => cb(Object.assign(new Error('nope'), { code: 'NoSuchBucket' })); + const wrapped = instrumentApiMethod(handler, 'bucketHead'); + + wrapped('foo', err => { + assert.strictEqual(err.message, 'nope'); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].status.code, SpanStatusCode.ERROR); + assert.strictEqual(spans[0].attributes['cloudserver.error_code'], 'NoSuchBucket'); + done(); + }); + }); + + it('ends span exactly once when callback fires then handler throws', () => { + // The first endSpan call (from the callback) wins; the + // post-callback throw is a programming bug, not an outcome + // of the API call — so status stays OK and we don't double- + // end (which would warn and corrupt span state). + const handler = cb => { + cb(null, 'first'); + throw new Error('after-callback-boom'); + }; + const wrapped = instrumentApiMethod(handler, 'objectPut'); + + let cbErr = null; + let cbValue = null; + const cb = (err, val) => { + cbErr = err; + cbValue = val; + }; + + assert.throws(() => wrapped(cb), /after-callback-boom/); + + assert.strictEqual(cbErr, null); + assert.strictEqual(cbValue, 'first'); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1, 'span ended exactly once'); + assert.strictEqual(spans[0].status.code, SpanStatusCode.OK); + }); + + it('synchronous throw before callback fires ends span and re-throws', () => { + const handler = () => { + throw new Error('sync-boom'); + }; + const wrapped = instrumentApiMethod(handler, 'objectDelete'); + + assert.throws(() => wrapped(() => {}), /sync-boom/); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].status.code, SpanStatusCode.ERROR); + }); + + it('wraps an async handler and ends span on resolution', async () => { + const handler = async a => `async-${a}`; + const wrapped = instrumentApiMethod(handler, 'objectGetAsync'); + + const value = await wrapped('x'); + assert.strictEqual(value, 'async-x'); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, 'api.objectGetAsync'); + assert.strictEqual(spans[0].status.code, SpanStatusCode.OK); + }); + + it('wraps an async handler and ends span with ERROR on rejection', async () => { + const handler = async () => { + const err = new Error('async-nope'); + err.code = 'NoSuchKey'; + throw err; + }; + const wrapped = instrumentApiMethod(handler, 'objectGetAsync'); + + await assert.rejects(wrapped(), /async-nope/); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].status.code, SpanStatusCode.ERROR); + assert.strictEqual(spans[0].attributes['cloudserver.error_code'], 'NoSuchKey'); + }); + + it('callback drives lifecycle even when handler also returns a Promise', done => { + // Hybrid shape (migration artifact): handler fires cb AND + // returns a Promise. The wrapper must NOT chain its own + // .then() onto that Promise — doing so would surface as an + // unhandled rejection in callback-only callers that discard + // the return value. + const handler = (a, cb) => { + cb(null, `cb-${a}`); + return Promise.resolve(`promise-${a}`); + }; + const wrapped = instrumentApiMethod(handler, 'hybridGet'); + + let cbErr; + let cbValue; + const returned = wrapped('x', (err, value) => { + cbErr = err; + cbValue = value; + }); + + assert.strictEqual(cbErr, null); + assert.strictEqual(cbValue, 'cb-x'); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].status.code, SpanStatusCode.OK); + + // Caller still receives the handler's original Promise + // (not a chained wrapper) so they can choose to await it. + assert.ok(returned && typeof returned.then === 'function'); + returned.then(v => { + assert.strictEqual(v, 'promise-x'); + done(); + }); + }); + + it('ends span on sync return when handler has no callback arg', () => { + const handler = (a, b) => `${a}-${b}`; + const wrapped = instrumentApiMethod(handler, 'objectRestore'); + + assert.strictEqual(wrapped('x', 'y'), 'x-y'); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].status.code, SpanStatusCode.OK); + }); + }); + + describe('OTEL off', () => { + // tracing.isEnabled() is checked at each instrumentApiMethod call, + // so flipping ENABLE_OTEL between calls is enough — no require.cache + // dance needed. + it('returns the original function unchanged', () => { + const saved = process.env.ENABLE_OTEL; + process.env.ENABLE_OTEL = 'false'; + try { + const handler = () => 'identity'; + assert.strictEqual(instrumentApiMethod(handler, 'foo'), handler); + } finally { + process.env.ENABLE_OTEL = saved; + } + }); + }); +}); diff --git a/tests/unit/lib/tracing/healthPaths.spec.js b/tests/unit/lib/tracing/healthPaths.spec.js new file mode 100644 index 0000000000..45707d5fe5 --- /dev/null +++ b/tests/unit/lib/tracing/healthPaths.spec.js @@ -0,0 +1,30 @@ +'use strict'; + +const assert = require('assert'); + +const { isHealthPath } = require('../../../../lib/tracing/healthPaths'); + +describe('tracing.isHealthPath', () => { + it('matches the canonical probe and scrape paths', () => { + ['/live', '/ready', '/_/healthcheck', '/_/healthcheck/deep', '/metrics'].forEach(p => + assert.strictEqual(isHealthPath(p), true, p), + ); + }); + + it('matches when a query string is present', () => { + assert.strictEqual(isHealthPath('/live?token=x'), true); + assert.strictEqual(isHealthPath('/metrics?format=prom'), true); + }); + + it('does not match unrelated paths', () => { + ['/bucket/key', '/_/backbeat/data/bucket/key', '/livez', '/metrics/custom'].forEach(p => + assert.strictEqual(isHealthPath(p), false, p), + ); + }); + + it('returns false for non-string / empty input', () => { + assert.strictEqual(isHealthPath(undefined), false); + assert.strictEqual(isHealthPath(''), false); + assert.strictEqual(isHealthPath(42), false); + }); +}); diff --git a/tests/unit/lib/tracing/init.spec.js b/tests/unit/lib/tracing/init.spec.js new file mode 100644 index 0000000000..22c4eee503 --- /dev/null +++ b/tests/unit/lib/tracing/init.spec.js @@ -0,0 +1,160 @@ +'use strict'; + +const assert = require('assert'); + +const TRACING_PATH = '../../../../lib/tracing'; + +function freshTracing() { + delete require.cache[require.resolve(TRACING_PATH)]; + return require(TRACING_PATH); +} + +describe('tracing.isEnabled', () => { + let saved; + beforeEach(() => { + saved = process.env.ENABLE_OTEL; + }); + afterEach(() => { + if (saved === undefined) { + delete process.env.ENABLE_OTEL; + } else { + process.env.ENABLE_OTEL = saved; + } + }); + + it('returns false when ENABLE_OTEL is unset', () => { + delete process.env.ENABLE_OTEL; + assert.strictEqual(freshTracing().isEnabled(), false); + }); + + it('returns false when ENABLE_OTEL is anything but "true"', () => { + process.env.ENABLE_OTEL = '1'; + assert.strictEqual(freshTracing().isEnabled(), false); + process.env.ENABLE_OTEL = 'TRUE'; + assert.strictEqual(freshTracing().isEnabled(), false); + process.env.ENABLE_OTEL = ''; + assert.strictEqual(freshTracing().isEnabled(), false); + }); + + it('returns true when ENABLE_OTEL is "true"', () => { + process.env.ENABLE_OTEL = 'true'; + assert.strictEqual(freshTracing().isEnabled(), true); + }); +}); + +describe('tracing.init', () => { + const savedEnv = { + ENABLE_OTEL: process.env.ENABLE_OTEL, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + }; + + afterEach(async () => { + // Tear down the SDK so the next test starts from a clean + // module-level state. We deliberately do NOT call + // trace/propagation/context.disable() here — those clear the + // process-global OTEL registry, which would also wipe the + // BasicTracerProvider that instrumentationSimple.spec.js set + // at module load. The next init() call will log a benign + // "duplicate registration" warning via diag; that is noise, + // not a functional issue. + try { + const t = require(TRACING_PATH); + await t.close(); + } catch (e) { + // ignore — close() before init is a no-op anyway + void e; + } + delete require.cache[require.resolve(TRACING_PATH)]; + + for (const [k, v] of Object.entries(savedEnv)) { + if (v === undefined) { + delete process.env[k]; + } else { + process.env[k] = v; + } + } + }); + + it('is a no-op when ENABLE_OTEL is unset', () => { + delete process.env.ENABLE_OTEL; + const t = freshTracing(); + assert.doesNotThrow(() => t.init()); + // close() should also short-circuit (no sdk to stop) + return t.close(); + }); + + it('throws when ENABLE_OTEL=true and the endpoint env var is unset', () => { + process.env.ENABLE_OTEL = 'true'; + delete process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT; + const t = freshTracing(); + assert.throws(() => t.init(), /OTEL_EXPORTER_OTLP_TRACES_ENDPOINT/); + }); + + it('boots the SDK when ENABLE_OTEL=true and an endpoint is set', () => { + process.env.ENABLE_OTEL = 'true'; + process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://otel-test.invalid:4318/v1/traces'; + const t = freshTracing(); + assert.doesNotThrow(() => t.init()); + assert.strictEqual(t.isEnabled(), true); + }); + + it('is idempotent — second init is a no-op', () => { + process.env.ENABLE_OTEL = 'true'; + process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://otel-test.invalid:4318/v1/traces'; + const t = freshTracing(); + t.init(); + // Second call must not throw and must not start a second SDK. + assert.doesNotThrow(() => t.init()); + }); +}); + +describe('tracing.close', () => { + const savedEnv = { + ENABLE_OTEL: process.env.ENABLE_OTEL, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + }; + + afterEach(async () => { + try { + const t = require(TRACING_PATH); + await t.close(); + } catch (e) { + void e; + } + delete require.cache[require.resolve(TRACING_PATH)]; + + for (const [k, v] of Object.entries(savedEnv)) { + if (v === undefined) { + delete process.env[k]; + } else { + process.env[k] = v; + } + } + }); + + it('resolves cleanly when init was never called', async () => { + delete process.env.ENABLE_OTEL; + const t = freshTracing(); + await t.close(); + }); + + it('resolves after init when enabled', async () => { + process.env.ENABLE_OTEL = 'true'; + process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://otel-test.invalid:4318/v1/traces'; + const t = freshTracing(); + t.init(); + await t.close(); + }); + + it('is safe under concurrent callers (race-safe)', async () => { + process.env.ENABLE_OTEL = 'true'; + process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://otel-test.invalid:4318/v1/traces'; + const t = freshTracing(); + t.init(); + // Capture two close() calls in flight at once. The race guard in + // tracing.close() clears the module-level sdk before awaiting, so + // sdk.shutdown() must run exactly once and both promises must + // resolve without throwing. + await Promise.all([t.close(), t.close()]); + }); +}); diff --git a/tests/unit/lib/tracing/trustedHosts.spec.js b/tests/unit/lib/tracing/trustedHosts.spec.js new file mode 100644 index 0000000000..e7b5a393db --- /dev/null +++ b/tests/unit/lib/tracing/trustedHosts.spec.js @@ -0,0 +1,266 @@ +'use strict'; + +const assert = require('assert'); + +const { buildTrustedHosts, extractHost, makeRequestHook } = require('../../../../lib/tracing/trustedHosts'); + +describe('tracing.extractHost', () => { + it('extracts hostname from a plain host string', () => { + assert.strictEqual(extractHost('example.com'), 'example.com'); + }); + + it('extracts hostname from a host:port string', () => { + assert.strictEqual(extractHost('example.com:8500'), 'example.com'); + }); + + it('lower-cases the extracted hostname', () => { + assert.strictEqual(extractHost('Example.COM:8500'), 'example.com'); + }); + + it('extracts hostname from an http URL', () => { + assert.strictEqual(extractHost('http://mongo.example.internal:27017/db'), 'mongo.example.internal'); + }); + + it('extracts hostname from an https URL', () => { + assert.strictEqual(extractHost('https://push.api.zenko.io/api/v1/instance'), 'push.api.zenko.io'); + }); + + it('returns undefined for empty / non-string input', () => { + assert.strictEqual(extractHost(undefined), undefined); + assert.strictEqual(extractHost(''), undefined); + assert.strictEqual(extractHost(42), undefined); + }); +}); + +describe('tracing.buildTrustedHosts', () => { + it('always contains loopback aliases', () => { + const hosts = buildTrustedHosts({}); + assert.ok(hosts.has('localhost')); + assert.ok(hosts.has('127.0.0.1')); + assert.ok(hosts.has('::1')); + }); + + it('handles a missing config gracefully', () => { + const hosts = buildTrustedHosts(); + assert.ok(hosts.has('localhost')); + assert.strictEqual(hosts.size, 3); + }); + + it('includes every host referenced in a full config', () => { + // Snapshot of every host-bearing config key cloudserver consults. + // If a new key is added without updating buildTrustedHosts, this + // test must fail. + const config = { + vaultd: { host: 'vaultd.zenko.svc.cluster.local' }, + dataClient: { host: 'data.zenko.svc.cluster.local' }, + metadataClient: { host: 'bucketd.zenko.svc.cluster.local' }, + pfsClient: { host: 'pfs.zenko.svc.cluster.local' }, + cdmi: { host: 'cdmi.zenko.svc.cluster.local' }, + bucketd: { + bootstrap: ['bucketd-a.zenko.svc.cluster.local:9000', 'bucketd-b.zenko.svc.cluster.local:9000'], + }, + kmip: { + transport: [ + { tls: { host: 'kmip-a.zenko.svc.cluster.local' } }, + { tls: { host: 'kmip-b.zenko.svc.cluster.local' } }, + ], + }, + kmsAWS: { endpoint: 'https://aws-kms.example.com' }, + scuba: { host: 'scuba.zenko.svc.cluster.local' }, + utapi: { host: 'utapi.zenko.svc.cluster.local' }, + localCache: { host: 'redis.zenko.svc.cluster.local' }, + managementAgent: { host: 'localhost' }, + backbeat: { host: 'backbeat.zenko.svc.cluster.local' }, + mongodb: { + replicaSetHosts: + 'mongo-0.zenko.svc.cluster.local:27017,' + + 'mongo-1.zenko.svc.cluster.local:27017,' + + 'mongo-2.zenko.svc.cluster.local:27017', + }, + }; + + const saved = { + PUSH_ENDPOINT: process.env.PUSH_ENDPOINT, + MANAGEMENT_ENDPOINT: process.env.MANAGEMENT_ENDPOINT, + }; + process.env.PUSH_ENDPOINT = 'https://push.api.zenko.io'; + process.env.MANAGEMENT_ENDPOINT = 'https://api.zenko.io'; + try { + const hosts = buildTrustedHosts(config); + const expected = [ + 'vaultd.zenko.svc.cluster.local', + 'data.zenko.svc.cluster.local', + 'bucketd.zenko.svc.cluster.local', + 'pfs.zenko.svc.cluster.local', + 'cdmi.zenko.svc.cluster.local', + 'bucketd-a.zenko.svc.cluster.local', + 'bucketd-b.zenko.svc.cluster.local', + 'kmip-a.zenko.svc.cluster.local', + 'kmip-b.zenko.svc.cluster.local', + 'aws-kms.example.com', + 'scuba.zenko.svc.cluster.local', + 'utapi.zenko.svc.cluster.local', + 'redis.zenko.svc.cluster.local', + 'backbeat.zenko.svc.cluster.local', + 'mongo-0.zenko.svc.cluster.local', + 'mongo-1.zenko.svc.cluster.local', + 'mongo-2.zenko.svc.cluster.local', + 'push.api.zenko.io', + 'api.zenko.io', + ]; + for (const h of expected) { + assert.ok(hosts.has(h), `expected trusted host ${h}`); + } + } finally { + if (saved.PUSH_ENDPOINT === undefined) { + delete process.env.PUSH_ENDPOINT; + } else { + process.env.PUSH_ENDPOINT = saved.PUSH_ENDPOINT; + } + if (saved.MANAGEMENT_ENDPOINT === undefined) { + delete process.env.MANAGEMENT_ENDPOINT; + } else { + process.env.MANAGEMENT_ENDPOINT = saved.MANAGEMENT_ENDPOINT; + } + } + }); + + it('tolerates a single-transport KMIP config object', () => { + const hosts = buildTrustedHosts({ + kmip: { transport: { tls: { host: 'kmip-single.example.com' } } }, + }); + assert.ok(hosts.has('kmip-single.example.com')); + }); + + it('ignores undefined host values', () => { + const hosts = buildTrustedHosts({ + vaultd: {}, + dataClient: {}, + metadataClient: {}, + }); + assert.strictEqual(hosts.size, 3); + }); + + it('includes hdclient and sproxyd connector hosts from locationConstraints, and only those', () => { + const hosts = buildTrustedHosts({ + locationConstraints: { + 'us-east-1': { + type: 'scality', + details: { + connector: { + hdclient: { + bootstrap: ['hdproxy-a.xcore.svc:18888', 'hdproxy-b.xcore.svc:18888'], + }, + }, + }, + }, + 'us-east-2': { + type: 'scality', + details: { + connector: { + sproxyd: { + bootstrap: ['sproxyd-a.ring.svc:81', 'sproxyd-b.ring.svc:81'], + }, + }, + }, + }, + 'aws-bucket': { + type: 'aws_s3', + details: { + awsEndpoint: 's3.us-west-2.amazonaws.com', + bucketName: 'external', + }, + }, + 'ring-remote': { + type: 'scality-ring-s3', + details: { + awsEndpoint: 's3.remote-ring.example.com', + bucketName: 'remote', + }, + }, + }, + }); + assert.ok(hosts.has('hdproxy-a.xcore.svc')); + assert.ok(hosts.has('hdproxy-b.xcore.svc')); + assert.ok(hosts.has('sproxyd-a.ring.svc')); + assert.ok(hosts.has('sproxyd-b.ring.svc')); + assert.ok(!hosts.has('s3.us-west-2.amazonaws.com')); + assert.ok(!hosts.has('s3.remote-ring.example.com')); + }); + + it('tolerates locationConstraints entries without a connector', () => { + assert.doesNotThrow(() => + buildTrustedHosts({ + locationConstraints: { + local: { type: 'file', details: {} }, + mem: { type: 'mem', details: {} }, + }, + }), + ); + }); +}); + +describe('tracing.makeRequestHook', () => { + const trusted = new Set(['trusted.example.com', 'localhost']); + const hook = makeRequestHook(trusted); + + function fakeClientRequest(host) { + const removed = []; + return { + _removed: removed, + getHeader(name) { + return name.toLowerCase() === 'host' ? host : undefined; + }, + removeHeader(name) { + removed.push(name); + }, + }; + } + + function fakeSpan() { + const attrs = {}; + return { + _attrs: attrs, + setAttribute(k, v) { + attrs[k] = v; + }, + }; + } + + it('is a no-op on inbound IncomingMessage (no getHeader method)', () => { + const span = fakeSpan(); + const inbound = { headers: { host: 'untrusted.example.com' } }; + assert.doesNotThrow(() => hook(span, inbound)); + assert.strictEqual(span._attrs['scality.trace.suppressed'], undefined); + }); + + it('is a no-op on undefined request', () => { + const span = fakeSpan(); + assert.doesNotThrow(() => hook(span, undefined)); + assert.strictEqual(span._attrs['scality.trace.suppressed'], undefined); + }); + + it('leaves trusted outbound requests untouched', () => { + const span = fakeSpan(); + const req = fakeClientRequest('trusted.example.com:8500'); + hook(span, req); + assert.deepStrictEqual(req._removed, []); + assert.strictEqual(span._attrs['scality.trace.suppressed'], undefined); + }); + + it('strips trace headers and tags span on untrusted outbound requests', () => { + const span = fakeSpan(); + const req = fakeClientRequest('external.example.com'); + hook(span, req); + assert.deepStrictEqual(req._removed.sort(), ['traceparent', 'tracestate']); + assert.strictEqual(span._attrs['scality.trace.suppressed'], true); + }); + + it('handles missing host header by treating as untrusted', () => { + const span = fakeSpan(); + const req = fakeClientRequest(undefined); + hook(span, req); + assert.deepStrictEqual(req._removed.sort(), ['traceparent', 'tracestate']); + assert.strictEqual(span._attrs['scality.trace.suppressed'], true); + }); +}); diff --git a/yarn.lock b/yarn.lock index 9ab9b0977f..04641d2313 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3221,6 +3221,24 @@ "@eslint/core" "^0.12.0" levn "^0.4.1" +"@grpc/grpc-js@^1.14.3": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.14.3.tgz#4c9b817a900ae4020ddc28515ae4b52c78cfb8da" + integrity sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA== + dependencies: + "@grpc/proto-loader" "^0.8.0" + "@js-sdsl/ordered-map" "^4.4.2" + +"@grpc/proto-loader@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz#b6c324dd909c458a0e4aa9bfd3d69cf78a4b9bd8" + integrity sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.5.3" + yargs "^17.7.2" + "@hapi/address@^4.0.1": version "4.1.0" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.1.0.tgz#d60c5c0d930e77456fdcde2598e77302e2955e1d" @@ -3395,6 +3413,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@js-sdsl/ordered-map@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" + integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== + "@js-sdsl/ordered-set@^4.4.2": version "4.4.2" resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-set/-/ordered-set-4.4.2.tgz#ab857eb63cf358b5a0f74fdd458b4601423779b7" @@ -3432,16 +3455,405 @@ dependencies: semver "^7.3.5" -"@opentelemetry/api@^1.4.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" - integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== +"@opentelemetry/api-logs@0.216.0": + version "0.216.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.216.0.tgz#7aa4b485ea2f2e21ffcb94120136c72f718c2eaf" + integrity sha512-KmGTgvxTJ0J01d4mOeX1wMV5NUTNf9HebIuOOGDfIn0a/IrnXIQbOnlylDyl9tkDv4h0DUpdI/GqCdLzfTkUXg== + dependencies: + "@opentelemetry/api" "^1.3.0" + +"@opentelemetry/api-logs@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz#7b9818e8dfdf1d3dcab88bfe4d6724f2f831f7ec" + integrity sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw== + dependencies: + "@opentelemetry/api" "^1.3.0" + +"@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.4.0", "@opentelemetry/api@^1.9.0": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.1.tgz#c1b0346de336ba55af2d5a7970882037baedec05" + integrity sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q== + +"@opentelemetry/configuration@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/configuration/-/configuration-0.218.0.tgz#a5fdd50ec9cfa0adb3bb41202cb39f79c5f4e7d9" + integrity sha512-W8wIz7H2R1pufR5jfjb3gU2XkMpm2x/7b1RJcsuzvd70Il/rWWE+g5/Od7hQKrxRTSrTrOWlru101PWXz5I1EQ== + dependencies: + "@opentelemetry/core" "2.7.1" + yaml "^2.0.0" + +"@opentelemetry/context-async-hooks@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-2.7.1.tgz#1555a6fb269596416d8c626fd020c3f2c38e071f" + integrity sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ== + +"@opentelemetry/core@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-2.7.1.tgz#162bfab46d6ff4da1bef240ea52e23a926b0fdbc" + integrity sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw== + dependencies: + "@opentelemetry/semantic-conventions" "^1.29.0" + +"@opentelemetry/exporter-logs-otlp-grpc@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.218.0.tgz#7e5a7e624074d6449590c851d58efe60096314b3" + integrity sha512-hoxrNH1l/Xy6F9WTJ5IK+6j1r9nQFlPOmrnTlhYHTySdunfXLmUCPv3bQtKYntxag9h3wLYBZQ2HI6FOx+BT2g== + dependencies: + "@grpc/grpc-js" "^1.14.3" + "@opentelemetry/core" "2.7.1" + "@opentelemetry/otlp-exporter-base" "0.218.0" + "@opentelemetry/otlp-grpc-exporter-base" "0.218.0" + "@opentelemetry/otlp-transformer" "0.218.0" + "@opentelemetry/sdk-logs" "0.218.0" + +"@opentelemetry/exporter-logs-otlp-http@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.218.0.tgz#0996cb45e0ebc6c7465445430a018edcac9871b4" + integrity sha512-Qx+4rpVHzgg89dawcWRHyt+XRXeLnhFz/qBtvggmjkcgPUdr+NAB0/u/eIPA8yAeJV0J80Vz43JZCh/XFvZFGw== + dependencies: + "@opentelemetry/api-logs" "0.218.0" + "@opentelemetry/core" "2.7.1" + "@opentelemetry/otlp-exporter-base" "0.218.0" + "@opentelemetry/otlp-transformer" "0.218.0" + "@opentelemetry/sdk-logs" "0.218.0" + +"@opentelemetry/exporter-logs-otlp-proto@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.218.0.tgz#e539720b2e145e85a7a4901a1eabb0b2e77990f8" + integrity sha512-1/noQNsp9gXD75HPzgjBrcF1+XTtry7pFAUfxVEJgg7mPv2AawKQuYkhMmJ8qjxz4Ubc3Y8bwvfxevXsKTq4cg== + dependencies: + "@opentelemetry/api-logs" "0.218.0" + "@opentelemetry/core" "2.7.1" + "@opentelemetry/otlp-exporter-base" "0.218.0" + "@opentelemetry/otlp-transformer" "0.218.0" + "@opentelemetry/resources" "2.7.1" + "@opentelemetry/sdk-logs" "0.218.0" + "@opentelemetry/sdk-trace-base" "2.7.1" + +"@opentelemetry/exporter-metrics-otlp-grpc@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.218.0.tgz#6d66c2638702489d063b8857741eee9cea1d21d6" + integrity sha512-YapQ9vNMX0NSZF6LK5pWAFfjpJleV2O9uYWfYGeb/5F1Kb9rPGK8tZDMJFa/sOksgdFuflDvYuA0B4qjDB4fjQ== + dependencies: + "@grpc/grpc-js" "^1.14.3" + "@opentelemetry/core" "2.7.1" + "@opentelemetry/exporter-metrics-otlp-http" "0.218.0" + "@opentelemetry/otlp-exporter-base" "0.218.0" + "@opentelemetry/otlp-grpc-exporter-base" "0.218.0" + "@opentelemetry/otlp-transformer" "0.218.0" + "@opentelemetry/resources" "2.7.1" + "@opentelemetry/sdk-metrics" "2.7.1" + +"@opentelemetry/exporter-metrics-otlp-http@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.218.0.tgz#c205581585d04c5106d1ca766e23632006e46a2c" + integrity sha512-bV7d2OuMpZu2+gAaxUAhzfZ0h3WVZk8ETQUEE3DNSntbTaMpuITjtm8I0rNyHFdm7Ax57K6ty7SgFXlBmOLIvQ== + dependencies: + "@opentelemetry/core" "2.7.1" + "@opentelemetry/otlp-exporter-base" "0.218.0" + "@opentelemetry/otlp-transformer" "0.218.0" + "@opentelemetry/resources" "2.7.1" + "@opentelemetry/sdk-metrics" "2.7.1" + +"@opentelemetry/exporter-metrics-otlp-proto@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.218.0.tgz#66642b8bb5e654ca7a5944a4f0b490814fc875fd" + integrity sha512-ubLddKjWULhla9YZRCj/rTBeppjJYE4e9w0icx5mTu3eFhWjQzbV75NYjXuIlEG+NJsBl6d+sTFw5Qu+oej4oQ== + dependencies: + "@opentelemetry/core" "2.7.1" + "@opentelemetry/exporter-metrics-otlp-http" "0.218.0" + "@opentelemetry/otlp-exporter-base" "0.218.0" + "@opentelemetry/otlp-transformer" "0.218.0" + "@opentelemetry/resources" "2.7.1" + "@opentelemetry/sdk-metrics" "2.7.1" + +"@opentelemetry/exporter-prometheus@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.218.0.tgz#41bddc97d34cd9d9993382b9c10fed19a8de63f4" + integrity sha512-RT5oEyu1kddZJ1vt7/BUo5wV+P7hpNAESsR3dUd3+8deHuX7gWNoCOZn+SfDT+hJHlIJ5h/AxiCLXIrutswDJg== + dependencies: + "@opentelemetry/core" "2.7.1" + "@opentelemetry/resources" "2.7.1" + "@opentelemetry/sdk-metrics" "2.7.1" + "@opentelemetry/semantic-conventions" "^1.29.0" + +"@opentelemetry/exporter-trace-otlp-grpc@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.218.0.tgz#5d5532ab94d5514bec8aedc19ce3170b6381eed1" + integrity sha512-3fXxVQEj9TNAFaCi79JeFKfeLd0sDtInaR3gaZDVlzNSPHtz8PZuCV34JKWjD4XXzT20IdMe8IpX6mRVNDA4Tw== + dependencies: + "@grpc/grpc-js" "^1.14.3" + "@opentelemetry/core" "2.7.1" + "@opentelemetry/otlp-exporter-base" "0.218.0" + "@opentelemetry/otlp-grpc-exporter-base" "0.218.0" + "@opentelemetry/otlp-transformer" "0.218.0" + "@opentelemetry/resources" "2.7.1" + "@opentelemetry/sdk-trace-base" "2.7.1" + +"@opentelemetry/exporter-trace-otlp-http@0.218.0", "@opentelemetry/exporter-trace-otlp-http@~0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.218.0.tgz#36d6abf6d639b9ea861603c61f434751c0b1a0ea" + integrity sha512-8dqezsmPhtKitIK/eTipZhYl9EX2/gNQ5zUMhaz3uxEURwfkNf8IPvo6yNfrzbxdtpAOybS/+h7wmIWYqFSpiw== + dependencies: + "@opentelemetry/core" "2.7.1" + "@opentelemetry/otlp-exporter-base" "0.218.0" + "@opentelemetry/otlp-transformer" "0.218.0" + "@opentelemetry/resources" "2.7.1" + "@opentelemetry/sdk-trace-base" "2.7.1" + +"@opentelemetry/exporter-trace-otlp-proto@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.218.0.tgz#de0b2b3545149dafedd2b948fb891f9bf962940c" + integrity sha512-r1Msf8SNLRmwh9J6XQ5uh82D7CdDWMNHnPB7LAVHjzut0TkSeKc5KcIvr4SvHvfk/xwN5gxC+VLKQ1k0o8PSPw== + dependencies: + "@opentelemetry/core" "2.7.1" + "@opentelemetry/otlp-exporter-base" "0.218.0" + "@opentelemetry/otlp-transformer" "0.218.0" + "@opentelemetry/resources" "2.7.1" + "@opentelemetry/sdk-trace-base" "2.7.1" + +"@opentelemetry/exporter-zipkin@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.7.1.tgz#3b79d223adc8c097ba3323e4de4ed8abb83c789e" + integrity sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg== + dependencies: + "@opentelemetry/core" "2.7.1" + "@opentelemetry/resources" "2.7.1" + "@opentelemetry/sdk-trace-base" "2.7.1" + "@opentelemetry/semantic-conventions" "^1.29.0" + +"@opentelemetry/instrumentation-http@~0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.218.0.tgz#0b26b702a6288fa11bdea48ff4a3635385a7d587" + integrity sha512-x9djaqdzpT8WAboep1H9nCAQ1E+MMsm08TNfA02TqM3bNNddZeiim+E3KMWVQFaX6JpUy7V0nm/wfN/K2Em+Zw== + dependencies: + "@opentelemetry/core" "2.7.1" + "@opentelemetry/instrumentation" "0.218.0" + "@opentelemetry/semantic-conventions" "^1.29.0" + forwarded-parse "2.1.2" + +"@opentelemetry/instrumentation-ioredis@~0.64.0": + version "0.64.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.64.0.tgz#b02a214263d5f3a6848f6911fe23f505ff6a9181" + integrity sha512-GQ36/amPdO1rVPXgrRZNnd6MktqwDcYalzpMRe9m55b3EwX4pazq8VB3qfTH67xboElqm/B9J1tBEnbQmcvaww== + dependencies: + "@opentelemetry/instrumentation" "^0.216.0" + "@opentelemetry/redis-common" "^0.38.3" + "@opentelemetry/semantic-conventions" "^1.33.0" + +"@opentelemetry/instrumentation-mongodb@~0.69.0": + version "0.69.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.69.0.tgz#b03010de06c816f973038165cfe4cea852b3d43f" + integrity sha512-kj8w2FN2/z0VIXMqcdAdJYtc0udH41Sb485jC7tLl0X4+OD3KLjyhjVoZOXH/gxp+N+BQY6SKgMNC0yi8nok9A== + dependencies: + "@opentelemetry/instrumentation" "^0.216.0" + "@opentelemetry/semantic-conventions" "^1.33.0" + +"@opentelemetry/instrumentation@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz#fcceb4ffb45f99c0d292600769150fc5944dc3e9" + integrity sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg== + dependencies: + "@opentelemetry/api-logs" "0.218.0" + import-in-the-middle "^3.0.0" + require-in-the-middle "^8.0.0" + +"@opentelemetry/instrumentation@^0.216.0": + version "0.216.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.216.0.tgz#048777113d0cb7f6b6613025055fc0d2f822bf49" + integrity sha512-BrY0b2K81OLgwBcFxY2wKgPFhq4DpindT+S83++zquc5Rtb2SuYLMkujgDRWMgZQDz+OT+dfvPnMGADPuw4FDw== + dependencies: + "@opentelemetry/api-logs" "0.216.0" + import-in-the-middle "^3.0.0" + require-in-the-middle "^8.0.0" + +"@opentelemetry/otlp-exporter-base@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz#f33e3217ce568756baa132fabe30f0c9345db086" + integrity sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA== + dependencies: + "@opentelemetry/core" "2.7.1" + "@opentelemetry/otlp-transformer" "0.218.0" + +"@opentelemetry/otlp-grpc-exporter-base@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.218.0.tgz#dc5a1fec716245d84dc4167de9b1c607e07fb6a7" + integrity sha512-H/lCGJ536N98VpYJOaWTQOkv4Dx6TnmStK6Rqfu1W7KkFbPAx04hjdYEMZF/YbnHzPUSIK4kM6OE2GKGBTpV9A== + dependencies: + "@grpc/grpc-js" "^1.14.3" + "@opentelemetry/core" "2.7.1" + "@opentelemetry/otlp-exporter-base" "0.218.0" + "@opentelemetry/otlp-transformer" "0.218.0" + +"@opentelemetry/otlp-transformer@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz#6784d0ddd13803c63a1b24072606c02fc21b9071" + integrity sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ== + dependencies: + "@opentelemetry/api-logs" "0.218.0" + "@opentelemetry/core" "2.7.1" + "@opentelemetry/resources" "2.7.1" + "@opentelemetry/sdk-logs" "0.218.0" + "@opentelemetry/sdk-metrics" "2.7.1" + "@opentelemetry/sdk-trace-base" "2.7.1" + +"@opentelemetry/propagator-b3@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-2.7.1.tgz#107fe3e16d0728c489edbad221c402ee197514a4" + integrity sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A== + dependencies: + "@opentelemetry/core" "2.7.1" + +"@opentelemetry/propagator-jaeger@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.7.1.tgz#e8ebf3f6c0e9aa525cf041893425889cf3e69125" + integrity sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q== + dependencies: + "@opentelemetry/core" "2.7.1" + +"@opentelemetry/redis-common@^0.38.3": + version "0.38.3" + resolved "https://registry.yarnpkg.com/@opentelemetry/redis-common/-/redis-common-0.38.3.tgz#31a0464a48a991c29408614e3725d94db7c11aee" + integrity sha512-VCghU1JYs/4gP6Gqf/xro9MEsZ7LrMv2uONVsaESKL38ZOB9BqnI98FfS23wjMnHlpuE+TTaWSoAVNpTwYXzjw== + +"@opentelemetry/resources@2.7.1", "@opentelemetry/resources@^2.7.0": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-2.7.1.tgz#3b2a9179f6119bb1f2cddefe41ba9b2855504a5d" + integrity sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ== + dependencies: + "@opentelemetry/core" "2.7.1" + "@opentelemetry/semantic-conventions" "^1.29.0" + +"@opentelemetry/sdk-logs@0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz#78886fe300b82802cee9963208b2af326b928af5" + integrity sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag== + dependencies: + "@opentelemetry/api-logs" "0.218.0" + "@opentelemetry/core" "2.7.1" + "@opentelemetry/resources" "2.7.1" + "@opentelemetry/semantic-conventions" "^1.29.0" + +"@opentelemetry/sdk-metrics@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz#b713f69dd67933ecc9c61357f1d452cdc9f4e281" + integrity sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ== + dependencies: + "@opentelemetry/core" "2.7.1" + "@opentelemetry/resources" "2.7.1" + +"@opentelemetry/sdk-node@~0.218.0": + version "0.218.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-node/-/sdk-node-0.218.0.tgz#cf8bdc0c993387189c7c474612e0f801505feb97" + integrity sha512-tPMjHrLV5gsfNdYqoRHjeGbCAZBXXD9c1Qo/2ut7VwnUABDNh76xNxrT0SEhkIIJuCN45bbN1vZnYL1gY0IkOg== + dependencies: + "@opentelemetry/api-logs" "0.218.0" + "@opentelemetry/configuration" "0.218.0" + "@opentelemetry/context-async-hooks" "2.7.1" + "@opentelemetry/core" "2.7.1" + "@opentelemetry/exporter-logs-otlp-grpc" "0.218.0" + "@opentelemetry/exporter-logs-otlp-http" "0.218.0" + "@opentelemetry/exporter-logs-otlp-proto" "0.218.0" + "@opentelemetry/exporter-metrics-otlp-grpc" "0.218.0" + "@opentelemetry/exporter-metrics-otlp-http" "0.218.0" + "@opentelemetry/exporter-metrics-otlp-proto" "0.218.0" + "@opentelemetry/exporter-prometheus" "0.218.0" + "@opentelemetry/exporter-trace-otlp-grpc" "0.218.0" + "@opentelemetry/exporter-trace-otlp-http" "0.218.0" + "@opentelemetry/exporter-trace-otlp-proto" "0.218.0" + "@opentelemetry/exporter-zipkin" "2.7.1" + "@opentelemetry/instrumentation" "0.218.0" + "@opentelemetry/otlp-exporter-base" "0.218.0" + "@opentelemetry/propagator-b3" "2.7.1" + "@opentelemetry/propagator-jaeger" "2.7.1" + "@opentelemetry/resources" "2.7.1" + "@opentelemetry/sdk-logs" "0.218.0" + "@opentelemetry/sdk-metrics" "2.7.1" + "@opentelemetry/sdk-trace-base" "2.7.1" + "@opentelemetry/sdk-trace-node" "2.7.1" + "@opentelemetry/semantic-conventions" "^1.29.0" + +"@opentelemetry/sdk-trace-base@2.7.1", "@opentelemetry/sdk-trace-base@^2.7.0": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz#9160c3af9ef2219c26563abd136e22fb7d19b34f" + integrity sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw== + dependencies: + "@opentelemetry/core" "2.7.1" + "@opentelemetry/resources" "2.7.1" + "@opentelemetry/semantic-conventions" "^1.29.0" + +"@opentelemetry/sdk-trace-node@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.7.1.tgz#54dedb8e77fa51a6d02fc2192097739266c82168" + integrity sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg== + dependencies: + "@opentelemetry/context-async-hooks" "2.7.1" + "@opentelemetry/core" "2.7.1" + "@opentelemetry/sdk-trace-base" "2.7.1" + +"@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.33.0": + version "1.40.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz#10b2944ca559386590683392022a897eefd011d3" + integrity sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw== "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.5.tgz#d9315ad7cf3f30aac70bda3c068443dc6f143659" + integrity sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0", "@protobufjs/inquire@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.1.tgz#6cb936f4ac50965230af1e9d0bbfd57ea3675aa4" + integrity sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.1.tgz#eaee5900122c110a3dbcb728c0597014a2621774" + integrity sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg== + "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" @@ -5808,6 +6220,13 @@ dependencies: undici-types "~6.20.0" +"@types/node@>=13.7.0": + version "25.6.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.0.tgz#4e09bad9b469871f2d0f68140198cbd714f4edca" + integrity sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ== + dependencies: + undici-types "~7.19.0" + "@types/triple-beam@^1.3.2": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c" @@ -5926,6 +6345,11 @@ accesscontrol@^2.2.1: dependencies: notation "^1.3.6" +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -5936,6 +6360,11 @@ acorn@^8.14.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== +acorn@^8.15.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" + integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== + agent-base@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" @@ -6210,9 +6639,9 @@ arraybuffer.prototype.slice@^1.0.4: optionalDependencies: ioctl "^2.0.2" -"arsenal@git+https://github.com/scality/Arsenal#8.4.1": - version "8.4.1" - resolved "git+https://github.com/scality/Arsenal#6b3b58b152ac23d29176ab1f24f49f8eda3145b2" +"arsenal@git+https://github.com/scality/Arsenal#improvement/ARSN-572/trace-context": + version "8.3.11" + resolved "git+https://github.com/scality/Arsenal#86191514c86e1dba0532c14ec43824ee912e7d08" dependencies: "@aws-sdk/client-kms" "^3.975.0" "@aws-sdk/client-s3" "^3.975.0" @@ -6221,6 +6650,7 @@ arraybuffer.prototype.slice@^1.0.4: "@azure/identity" "^4.13.0" "@azure/storage-blob" "^12.31.0" "@js-sdsl/ordered-set" "^4.4.2" + "@opentelemetry/api" "^1.9.0" "@scality/hdclient" "^1.3.2" "@smithy/node-http-handler" "^4.3.0" "@smithy/protocol-http" "^5.3.5" @@ -6706,6 +7136,11 @@ chownr@^3.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4" integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== +cjs-module-lexer@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz#b3ca5101843389259ade7d88c77bd06ce55849ca" + integrity sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ== + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -8010,6 +8445,11 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +forwarded-parse@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/forwarded-parse/-/forwarded-parse-2.1.2.tgz#08511eddaaa2ddfd56ba11138eee7df117a09325" + integrity sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw== + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -8534,6 +8974,16 @@ import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" +import-in-the-middle@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-3.0.1.tgz#8a0a1230c9b865c0e12698171646ae1e3fff691d" + integrity sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA== + dependencies: + acorn "^8.15.0" + acorn-import-attributes "^1.9.5" + cjs-module-lexer "^2.2.0" + module-details-from-path "^1.0.4" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -9503,6 +9953,11 @@ lodash-compat@^3.10.2: resolved "https://registry.yarnpkg.com/lodash-compat/-/lodash-compat-3.10.2.tgz#c6940128a9d30f8e902cd2cf99fd0cba4ecfc183" integrity sha512-k8SE/OwvWfYZqx3MA/Ry1SHBDWre8Z8tCs0Ba0bF5OqVNvymxgFZ/4VDtbTxzTvcoG11JpTMFsaeZp/yGYvFnA== +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" @@ -9620,6 +10075,11 @@ long-timeout@0.1.1: resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" integrity sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w== +long@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== + looper@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/looper/-/looper-2.0.0.tgz#66cd0c774af3d4fedac53794f742db56da8f09ec" @@ -9963,6 +10423,11 @@ mocha@^11.7.5: yargs-parser "^21.1.1" yargs-unparser "^2.0.0" +module-details-from-path@^1.0.3, module-details-from-path@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.4.tgz#b662fdcd93f6c83d3f25289da0ce81c8d9685b94" + integrity sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w== + moment@^2.30.1: version "2.30.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" @@ -10656,6 +11121,24 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" +protobufjs@^7.5.3: + version "7.5.6" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.6.tgz#11af832ebc4b4326f658a5b1308e6141eb57edfd" + integrity sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.5" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.1" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.1" + "@types/node" ">=13.7.0" + long "^5.0.0" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -10922,6 +11405,14 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-in-the-middle@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz#dbde2587f669398626d56b20c868ab87bf01cce4" + integrity sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ== + dependencies: + debug "^4.3.5" + module-details-from-path "^1.0.3" + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -11985,6 +12476,11 @@ undici-types@~6.20.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== +undici-types@~7.19.0: + version "7.19.2" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.19.2.tgz#1b67fc26d0f157a0cba3a58a5b5c1e2276b8ba2a" + integrity sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg== + unique-filename@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-4.0.0.tgz#a06534d370e7c977a939cd1d11f7f0ab8f1fed13" @@ -12448,6 +12944,11 @@ yallist@^5.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533" integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== +yaml@^2.0.0: + version "2.8.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.4.tgz#4b5f411dd25f9544914d8673d4da7f29248e5e2e" + integrity sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog== + yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"