@instana/core
Version:
Core library for Instana's Node.js packages
500 lines (463 loc) • 12.7 kB
JavaScript
/*
* (c) Copyright IBM Corp. 2021
* (c) Copyright Instana Inc. and contributors 2020
*/
;
const path = require('path');
const shimmer = require('../../../shimmer');
const cls = require('../../../cls');
const constants = require('../../../constants');
const hook = require('../../../../util/hook');
const tracingUtil = require('../../../tracingUtil');
let isActive = false;
exports.init = function init() {
hook.onModuleLoad('@google-cloud/storage', instrument);
};
const storageInstrumentations = [
{
method: 'createBucket',
operation: 'buckets.insert',
extractorPre: (gcs, ctx, originalArgs) => {
gcs.bucket = originalArgs[0];
}
},
{
method: 'getBuckets',
operation: 'buckets.list'
},
{
method: 'getServiceAccount',
operation: 'serviceAccount.get'
},
{
method: 'createHmacKey',
operation: 'hmacKeys.create',
extractorPost: (gcs, result) => {
if (result && result.metadata) {
gcs.projectId = result.metadata.projectId;
gcs.accessId = result.metadata.accessId;
}
}
}
];
const bucketInstrumentations = [
{
method: 'addLifecycleRule',
operation: 'buckets.patch',
extractorPre: bucketNameExtractor
},
{
method: 'combine',
operation: 'objects.compose',
extractorPre: (gcs, ctx, originalArgs) => {
const destination = originalArgs[1];
gcs.destinationBucket = bucketNameFromFileOrString(destination, ctx.name);
gcs.destinationObject = fileNameFromFileOrString(destination);
}
},
{
method: 'delete',
operation: 'buckets.delete',
extractorPre: bucketNameExtractor
},
{
method: 'deleteFiles',
operation: 'objects.delete',
extractorPre: bucketNameExtractor
},
{
method: 'deleteLabels',
operation: 'buckets.patch',
extractorPre: bucketNameExtractor
},
{
method: 'disableRequesterPays',
operation: 'buckets.patch',
extractorPre: bucketNameExtractor
},
{
method: 'enableLogging',
operation: 'buckets.patch',
extractorPre: bucketNameExtractor
},
{
method: 'enableRequesterPays',
operation: 'buckets.patch',
extractorPre: bucketNameExtractor
},
{
method: 'exists',
operation: 'buckets.get',
extractorPre: bucketNameExtractor
},
{
method: 'get',
operation: 'buckets.get',
extractorPre: bucketNameExtractor
},
{
method: 'getFiles',
operation: 'objects.list',
extractorPre: bucketNameExtractor
},
{
method: 'getLabels',
operation: 'buckets.get',
extractorPre: bucketNameExtractor
},
{
method: 'getMetadata',
operation: 'buckets.get',
extractorPre: bucketNameExtractor
},
{
method: 'getNotifications',
operation: 'notifications.get',
extractorPre: bucketNameExtractor
},
{
method: 'getSignedUrl',
operation: 'buckets.get',
extractorPre: bucketNameExtractor
},
{
method: 'lock',
operation: 'buckets.patch',
extractorPre: bucketNameExtractor
},
{
method: 'makePrivate',
operation: 'buckets.patch',
extractorPre: bucketNameExtractor
},
{
method: 'makePublic',
operation: 'buckets.patch',
extractorPre: bucketNameExtractor
},
{
method: 'removeRetentionPeriod',
operation: 'buckets.patch',
extractorPre: bucketNameExtractor
},
{
method: 'setCorsConfiguration',
operation: 'buckets.patch',
extractorPre: bucketNameExtractor
},
{
method: 'setLabels',
operation: 'buckets.patch',
extractorPre: bucketNameExtractor
},
{
method: 'setMetadata',
operation: 'buckets.patch',
extractorPre: bucketNameExtractor
},
{
method: 'setRetentionPeriod',
operation: 'buckets.patch',
extractorPre: bucketNameExtractor
},
{
method: 'setStorageClass',
operation: 'buckets.patch',
extractorPre: bucketNameExtractor
},
{
method: 'upload',
operation: 'objects.insert',
extractorPre: (gcs, ctx, originalArgs) => {
gcs.bucket = ctx.name;
const options = originalArgs[1] && typeof originalArgs[1] === 'object' ? originalArgs[1] : {};
gcs.object = options.destination ? options.destination : path.basename(originalArgs[0]);
}
}
];
function bucketNameExtractor(gcs, ctx) {
gcs.bucket = ctx.name;
}
const fileInstrumentations = [
{
method: 'copy',
operation: 'objects.rewrite',
extractorPre: copyOrMoveExtractor
},
{
method: 'delete',
operation: 'objects.delete',
extractorPre: bucketAndObjectFromFileExtractor
},
{
method: 'download',
operation: 'objects.get',
extractorPre: bucketAndObjectFromFileExtractor
},
{
method: 'exists',
operation: 'objects.get',
extractorPre: bucketAndObjectFromFileExtractor
},
{
method: 'get',
operation: 'objects.get',
extractorPre: bucketAndObjectFromFileExtractor
},
{
method: 'getExpirationDate',
operation: 'objects.get',
extractorPre: bucketAndObjectFromFileExtractor
},
{
method: 'getMetadata',
operation: 'objects.get',
extractorPre: bucketAndObjectFromFileExtractor
},
{
method: 'getSignedUrl',
operation: 'upload.openSignedUrl',
extractorPre: bucketAndObjectFromFileExtractor
},
{
method: 'isPublic',
operation: 'objects.get',
extractorPre: bucketAndObjectFromFileExtractor
},
{
method: 'makePublic',
operation: 'objects.patch',
extractorPre: bucketAndObjectFromFileExtractor
},
{
method: 'makePrivate',
operation: 'objects.patch',
extractorPre: bucketAndObjectFromFileExtractor
},
{
method: 'move',
operation: 'objects.rewrite',
extractorPre: copyOrMoveExtractor
},
{
method: 'rotateEncryptionKey',
operation: 'objects.patch',
extractorPre: bucketAndObjectFromFileExtractor
},
{
method: 'setMetadata',
operation: 'objects.patch',
extractorPre: bucketAndObjectFromFileExtractor
},
{
method: 'setStorageClass',
operation: 'objects.rewrite',
extractorPre: (gcs, ctx) => {
gcs.sourceBucket = ctx.bucket ? ctx.bucket.name : undefined;
gcs.sourceObject = ctx.name;
gcs.destinationBucket = ctx.bucket ? ctx.bucket.name : undefined;
gcs.destinationObject = ctx.name;
}
}
];
function bucketAndObjectFromFileExtractor(gcs, ctx) {
gcs.bucket = ctx.bucket ? ctx.bucket.name : undefined;
gcs.object = ctx.name;
}
function copyOrMoveExtractor(gcs, ctx, originalArgs) {
gcs.sourceBucket = ctx.bucket ? ctx.bucket.name : undefined;
gcs.sourceObject = ctx.name;
if (typeof originalArgs[0] === 'string') {
gcs.destinationBucket = ctx.bucket ? ctx.bucket.name : undefined;
gcs.destinationObject = originalArgs[0];
} else if (originalArgs[0] && originalArgs[0].constructor && originalArgs[0].constructor.name === 'File') {
gcs.destinationBucket = originalArgs[0].bucket ? originalArgs[0].bucket.name : undefined;
gcs.destinationObject = originalArgs[0].name;
} else if (originalArgs[0] && originalArgs[0].constructor && originalArgs[0].constructor.name === 'Bucket') {
gcs.destinationBucket = originalArgs[0].name;
gcs.destinationObject = gcs.sourceObject;
}
}
const hmacKeyInstrumentations = [
{
method: 'delete',
operation: 'hmacKeys.delete',
extractorPre: hmacExtractor
},
{
method: 'get',
operation: 'hmacKeys.get',
extractorPre: hmacExtractor
},
{
method: 'getMetadata',
operation: 'hmacKeys.get',
extractorPre: hmacExtractor
},
{
method: 'setMetadata',
operation: 'hmacKeys.update',
extractorPre: hmacExtractor
}
];
function hmacExtractor(gcs, ctx) {
if (ctx.metadata) {
gcs.projectId = ctx.metadata.projectId;
gcs.accessId = ctx.metadata.accessId;
}
}
function instrument(storage) {
if (!storage.Storage) {
return;
}
if (storage.Storage.prototype) {
storageInstrumentations.forEach(config =>
shimmer.wrap(
storage.Storage.prototype,
config.method,
shim.bind(null, instrumentedOperation.bind(null, config.operation, config.extractorPre, config.extractorPost))
)
);
}
if (storage.Bucket.prototype) {
bucketInstrumentations.forEach(config =>
shimmer.wrap(
storage.Bucket.prototype,
config.method,
shim.bind(null, instrumentedOperation.bind(null, config.operation, config.extractorPre, config.extractorPost))
)
);
}
if (storage.File.prototype) {
fileInstrumentations.forEach(config =>
shimmer.wrap(
storage.File.prototype,
config.method,
shim.bind(null, instrumentedOperation.bind(null, config.operation, config.extractorPre, config.extractorPost))
)
);
shimmer.wrap(
storage.File.prototype,
'createReadStream',
shim.bind(null, instrumentedCreateStream.bind(null, 'objects.get', 'reading', 'end'))
);
shimmer.wrap(
storage.File.prototype,
'createWriteStream',
shim.bind(null, instrumentedCreateStream.bind(null, 'objects.insert', 'writing', 'finish'))
);
}
if (storage.HmacKey.prototype) {
hmacKeyInstrumentations.forEach(config =>
shimmer.wrap(
storage.HmacKey.prototype,
config.method,
shim.bind(null, instrumentedOperation.bind(null, config.operation, config.extractorPre, config.extractorPost))
)
);
}
}
function shim(instrumented, original) {
return function () {
if (cls.skipExitTracing({ isActive })) {
return original.apply(this, arguments);
}
const originalArgs = new Array(arguments.length);
for (let i = 0; i < arguments.length; i++) {
originalArgs[i] = arguments[i];
}
return instrumented(this, original, originalArgs);
};
}
function instrumentedOperation(operation, extractorPre, extractorPost, ctx, original, originalArgs) {
return cls.ns.runAndReturn(() => {
const span = cls.startSpan({
spanName: 'gcs',
kind: constants.EXIT
});
span.stack = tracingUtil.getStackTrace(instrumentedOperation, 1);
span.data.gcs = {
op: operation
};
if (extractorPre) {
extractorPre(span.data.gcs, ctx, originalArgs);
}
const callbackIndex = originalArgs.length - 1;
if (callbackIndex >= 0 && typeof originalArgs[callbackIndex] === 'function') {
const originalCallback = originalArgs[callbackIndex];
originalArgs[callbackIndex] = cls.ns.bind(function (error, result) {
finishSpan(error, result, span, extractorPost);
return originalCallback.apply(this, arguments);
});
}
const promise = original.apply(ctx, originalArgs);
if (promise) {
promise.then(
result => finishSpan(null, Array.isArray(result) ? result[0] : result, span, extractorPost),
e => finishSpan(e, null, span, extractorPost)
);
}
return promise;
});
}
function instrumentedCreateStream(operation, bindEvent, finalEvent, ctx, original, originalArgs) {
return cls.ns.runAndReturn(() => {
const span = cls.startSpan({
spanName: 'gcs',
kind: constants.EXIT
});
span.stack = tracingUtil.getStackTrace(instrumentedCreateStream, 1);
span.data.gcs = {
op: operation
};
bucketAndObjectFromFileExtractor(span.data.gcs, ctx);
const stream = original.apply(ctx, originalArgs);
if (stream) {
cls.ns.bindEmitter(stream);
// retroactively bind the existing main listener to keep the cls context
stream.listeners(bindEvent).forEach(listener => {
stream.removeListener(bindEvent, listener);
stream.on(bindEvent, cls.ns.bind(listener));
});
stream.on(finalEvent, () => finishSpan(null, null, span));
stream.on('error', err => finishSpan(err, null, span));
}
return stream;
});
}
function finishSpan(error, result, span, extractorPost) {
if (error) {
span.ec = 1;
tracingUtil.setErrorDetails(span, error, 'gcs');
}
if (extractorPost) {
extractorPost(span.data.gcs, result);
}
span.d = Date.now() - span.ts;
span.transmit();
}
function fileNameFromFileOrString(file) {
if (!file) {
return undefined;
} else if (typeof file === 'string') {
return file;
} else if (file.name) {
return file.name;
}
}
function bucketNameFromFileOrString(file, defaultBucket) {
if (!file) {
return undefined;
} else if (typeof file === 'string') {
return defaultBucket;
} else if (file.bucket && file.bucket.name) {
return file.bucket;
}
return defaultBucket;
}
exports.activate = function activate() {
isActive = true;
};
exports.deactivate = function deactivate() {
isActive = false;
};