Skip to content

Commit

Permalink
feat: embed Aspecto instrumentations (#754)
Browse files Browse the repository at this point in the history
* feat: add testing harness and kafkajs instrumentation port

* feat: port elasticsearch instrumentation

* feat: add typeorm port

* chore: readd fsevents to package-lock

* feat: port sequelize instrumentation

* chore: fix deps and dev imports

* feat: remove version files, fix compilation, remove use of arguments

* chore: setup python in ci steps

* chore: ci debug

* chore: set up older python version

* chore: upgrade python version

* chore: increase python version

* chore: try python 3 via apt

* downgrade ubuntu

* python 3 please

* set up python after node

* dont set up python

* ignore optional deps

* try apt

* downgrade ubuntu

* try apt get

* use 14-buster

* platform check python versions

* check python versions on centos

* remove python version checks on centos

* test without sqlite3

* mock sqlite for tests

* revert gh ci changes

* debug windows tests

* mocha debug

* rever debug changes

* skip some elasticsearch tests on windows

* add NOTICE

* combine elastic attributes into a single file

* readd node 14.0.0

* remove timeout from isntrumentations tests
  • Loading branch information
seemk authored Jul 17, 2023
1 parent cd86c08 commit 6def1aa
Show file tree
Hide file tree
Showing 38 changed files with 7,787 additions and 2,247 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Install npm dependencies
run: npm ci --ignore-scripts
run: npm ci --ignore-scripts --no-optional
- name: Prebuild
run: npm run prebuild:os ${{ matrix.node_api_target }}
- name: upload prebuilds
Expand Down Expand Up @@ -137,7 +137,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Install npm dependencies
run: npm ci
run: npm ci --ignore-scripts --no-optional
- name: Compile native
run: npm run prebuild:current
- name: Run tests
Expand Down
6 changes: 6 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Splunk's Distribution of OpenTelemetry for Node.js
Copyright 2023 Splunk Inc.

Parts of the library, such as elasticsearch, kafkajs, sequelize and typeorm instrumentations,
are copied and derived from instrumentations (https://github.com/aspecto-io/opentelemetry-ext-js) created by Aspecto
(https://www.aspecto.io/).
5,194 changes: 2,967 additions & 2,227 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 12 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
"lint:fix": "eslint . --ext .ts --fix",
"lint": "eslint . --ext .ts",
"lint:commits": "commitlint",
"test": "npm run test:unit && npm run test:debug-metrics",
"test:unit": "cross-env TEST_ALLOW_DOUBLE_START=y nyc ts-mocha --exclude 'test/separate_process/*' --timeout 60s --parallel --jobs 2 -p tsconfig.json 'test/**/*.test.ts'",
"test": "npm run test:unit && npm run test:debug-metrics && npm run test:instrumentations",
"test:unit": "cross-env TEST_ALLOW_DOUBLE_START=y nyc ts-mocha --exclude 'test/instrumentation/external/**/*.test.ts' --exclude 'test/separate_process/*' --timeout 60s --parallel --jobs 2 -p tsconfig.json 'test/**/*.test.ts'",
"test:debug-metrics": "nyc --no-clean ts-mocha -p tsconfig.json 'test/separate_process/debug_metrics.test.ts'",
"test:instrumentations": "nyc ts-mocha --require test/instrumentation/external/setup.ts --jobs 1 'test/instrumentation/external/**/*.test.ts'",
"prebuild:current": "node scripts/prebuild-current.js",
"prebuild:os": "node scripts/prebuild-os.js",
"profile:proto": "pbjs -t static-module -w commonjs -o src/profiling/proto/profile.js protos/pprof/profile.proto",
Expand Down Expand Up @@ -63,6 +64,7 @@
"devDependencies": {
"@commitlint/cli": "^17.4.4",
"@commitlint/config-conventional": "^17.4.4",
"@elastic/elasticsearch": "^7.17.11",
"@types/bunyan": "1.8.8",
"@types/mocha": "10.0.1",
"@types/semver": "^7.5.0",
Expand All @@ -78,18 +80,24 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"gts": "^3.1.1",
"kafkajs": "^1.12.0",
"mocha": "^10.2.0",
"mysql2": "^2.2.5",
"nock": "^13.3.1",
"nyc": "15.1.0",
"octokit": "^2.0.14",
"pg": "^8.4.2",
"pino": "^8.7.0",
"prebuildify": "^5.0.1",
"prettier": "^2.8.4",
"redis": "^3.1.2",
"rewire": "^6.0.0",
"sequelize": "^6.32.1",
"sinon": "^15.0.2",
"ts-mocha": "^10.0.0",
"ts-node": "^10.9.1",
"typescript": "5.0.4",
"typeorm": "^0.3.17",
"typescript": "5.1.6",
"winston": "3.8.2"
},
"dependencies": {
Expand Down Expand Up @@ -141,12 +149,9 @@
"@opentelemetry/sdk-trace-base": "1.14.0",
"@opentelemetry/sdk-trace-node": "1.14.0",
"@opentelemetry/semantic-conventions": "1.14.0",
"is-promise": "^4.0.0",
"nan": "^2.17.0",
"node-gyp-build": "^4.6.0",
"opentelemetry-instrumentation-elasticsearch": "^0.35.0",
"opentelemetry-instrumentation-kafkajs": "^0.35.0",
"opentelemetry-instrumentation-sequelize": "^0.35.0",
"opentelemetry-instrumentation-typeorm": "^0.35.0",
"protobufjs": "^7.2.2",
"semver": "^7.5.3"
},
Expand Down
231 changes: 231 additions & 0 deletions src/instrumentations/external/elasticsearch/elasticsearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { diag, context, trace, Span } from '@opentelemetry/api';
import { suppressTracing } from '@opentelemetry/core';
import type * as elasticsearch from '@elastic/elasticsearch';
import { ElasticsearchInstrumentationConfig } from './types';
import {
InstrumentationBase,
InstrumentationModuleDefinition,
InstrumentationNodeModuleDefinition,
InstrumentationNodeModuleFile,
} from '@opentelemetry/instrumentation';
import { VERSION } from '../../../version';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import {
startSpan,
onError,
onResponse,
defaultDbStatementSerializer,
normalizeArguments,
getIndexName,
} from './utils';
import { ELASTICSEARCH_API_FILES } from './helpers';

enum AttributeNames {
ELASTICSEARCH_INDICES = 'elasticsearch.request.indices',
}

export class ElasticsearchInstrumentation extends InstrumentationBase<
typeof elasticsearch
> {
static readonly component = '@elastic/elasticsearch';

protected override _config: ElasticsearchInstrumentationConfig = {};
private _isEnabled = false;
private moduleVersion?: string;

constructor(config: ElasticsearchInstrumentationConfig = {}) {
super(
'splunk-opentelemetry-instrumentation-elasticsearch',
VERSION,
Object.assign({}, config)
);
}

override setConfig(config: ElasticsearchInstrumentationConfig = {}) {
this._config = Object.assign({}, config);
}

protected init(): InstrumentationModuleDefinition<typeof elasticsearch> {
const apiModuleFiles = ELASTICSEARCH_API_FILES.map(
({ path, operationClassName }) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new InstrumentationNodeModuleFile<any>(
`@elastic/elasticsearch/api/${path}`,
['>=5 <8'],
(moduleExports, moduleVersion) => {
diag.debug(
`elasticsearch instrumentation: patch elasticsearch ${operationClassName}.`
);
this.moduleVersion = moduleVersion;
this._isEnabled = true;

const modulePrototypeKeys = Object.keys(moduleExports.prototype);
if (modulePrototypeKeys.length > 0) {
modulePrototypeKeys.forEach((functionName) => {
this._wrap(
moduleExports.prototype,
functionName,
this.wrappedApiRequest(operationClassName, functionName)
);
});
return moduleExports;
}

// For versions <= 7.9.0
const instrumentation = this;
return function (opts: unknown) {
const module = moduleExports(opts);
instrumentation.patchObject(operationClassName, module);
return module;
};
},
(moduleExports) => {
diag.debug(`elasticsearch instrumentation: unpatch elasticsearch.`);
this._isEnabled = false;

const modulePrototypeKeys = Object.keys(moduleExports.prototype);
if (modulePrototypeKeys.length > 0) {
modulePrototypeKeys.forEach((functionName) => {
this._unwrap(moduleExports.prototype, functionName);
});
} else {
// Unable to unwrap function for versions <= 7.9.0. Using _isEnabled flag instead.
}
}
)
);

const module = new InstrumentationNodeModuleDefinition<
typeof elasticsearch
>(
ElasticsearchInstrumentation.component,
['*'],
undefined,
undefined,
apiModuleFiles
);

return module;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private patchObject(operationClassName: string, object: any) {
Object.keys(object).forEach((functionName) => {
if (typeof object[functionName] === 'object') {
this.patchObject(functionName, object[functionName]);
} else {
this._wrap(
object,
functionName,
this.wrappedApiRequest(operationClassName, functionName)
);
}
});
}

private wrappedApiRequest(apiClassName: string, functionName: string) {
return (original: Function) => {
const instrumentation = this;
return function (this: unknown, ...args: unknown[]) {
if (!instrumentation._isEnabled) {
return original.apply(this, args);
}

const [params, options, originalCallback] = normalizeArguments(
args[0],
args[1],
args[2]
);
const operation = `${apiClassName}.${functionName}`;
const span = startSpan({
tracer: instrumentation.tracer,
attributes: {
[SemanticAttributes.DB_OPERATION]: operation,
[AttributeNames.ELASTICSEARCH_INDICES]: getIndexName(params),
[SemanticAttributes.DB_STATEMENT]: (
instrumentation._config.dbStatementSerializer ||
defaultDbStatementSerializer
)(operation, params, options),
},
});
instrumentation._addModuleVersionIfNeeded(span);

if (originalCallback) {
const wrappedCallback = function (
this: unknown,
err: Error,
result: elasticsearch.ApiResponse
) {
if (err) {
onError(span, err);
} else {
onResponse(span, result, instrumentation._config.responseHook);
}

return originalCallback.call(this, err, result);
};

return instrumentation._callOriginalFunction(span, () =>
original.call(this, params, options, wrappedCallback)
);
} else {
const promise = instrumentation._callOriginalFunction(span, () =>
original.apply(this, args)
);
promise.then(
(result: elasticsearch.ApiResponse) => {
onResponse(span, result, instrumentation._config.responseHook);
return result;
},
(err: Error) => {
onError(span, err);
return err;
}
);

return promise;
}
};
};
}

private _callOriginalFunction<T>(
span: Span,
originalFunction: (...args: unknown[]) => T
): T {
if (this._config?.suppressInternalInstrumentation) {
return context.with(suppressTracing(context.active()), originalFunction);
} else {
const activeContextWithSpan = trace.setSpan(context.active(), span);
return context.with(activeContextWithSpan, originalFunction);
}
}

private _addModuleVersionIfNeeded(span: Span) {
if (this.moduleVersion === undefined) {
return;
}

if (this._config.moduleVersionAttributeName) {
span.setAttribute(
this._config.moduleVersionAttributeName,
this.moduleVersion
);
}
}
}
51 changes: 51 additions & 0 deletions src/instrumentations/external/elasticsearch/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const ELASTICSEARCH_API_FILES = [
{ path: 'index.js', operationClassName: 'client' },
{ path: 'api/async_search.js', operationClassName: 'asyncSearch' },
{ path: 'api/autoscaling.js', operationClassName: 'autoscaling' },
{ path: 'api/cat.js', operationClassName: 'cat' },
{ path: 'api/ccr.js', operationClassName: 'ccr' },
{ path: 'api/cluster.js', operationClassName: 'cluster' },
{ path: 'api/dangling_indices.js', operationClassName: 'dangling_indices' },
{ path: 'api/enrich.js', operationClassName: 'enrich' },
{ path: 'api/eql.js', operationClassName: 'eql' },
{ path: 'api/graph.js', operationClassName: 'graph' },
{ path: 'api/ilm.js', operationClassName: 'ilm' },
{ path: 'api/indices.js', operationClassName: 'indices' },
{ path: 'api/ingest.js', operationClassName: 'ingest' },
{ path: 'api/license.js', operationClassName: 'license' },
{ path: 'api/logstash.js', operationClassName: 'logstash' },
{ path: 'api/migration.js', operationClassName: 'migration' },
{ path: 'api/ml.js', operationClassName: 'ml' },
{ path: 'api/monitoring.js', operationClassName: 'monitoring' },
{ path: 'api/nodes.js', operationClassName: 'nodes' },
{ path: 'api/rollup.js', operationClassName: 'rollup' },
{
path: 'api/searchable_snapshots.js',
operationClassName: 'searchable_snapshots',
},
{ path: 'api/security.js', operationClassName: 'security' },
{ path: 'api/slm.js', operationClassName: 'slm' },
{ path: 'api/snapshot.js', operationClassName: 'snapshot' },
{ path: 'api/sql.js', operationClassName: 'sql' },
{ path: 'api/ssl.js', operationClassName: 'ssl' },
{ path: 'api/tasks.js', operationClassName: 'tasks' },
{ path: 'api/text_structure.js', operationClassName: 'text_structure' },
{ path: 'api/transform.js', operationClassName: 'transform' },
{ path: 'api/watcher.js', operationClassName: 'watcher' },
{ path: 'api/xpack.js', operationClassName: 'xpack' },
];
17 changes: 17 additions & 0 deletions src/instrumentations/external/elasticsearch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './elasticsearch';
export * from './types';
Loading

0 comments on commit 6def1aa

Please sign in to comment.