diff --git a/examples/fastify-postgres/solarwinds.apm.config.js b/examples/fastify-postgres/solarwinds.apm.config.js new file mode 100644 index 00000000..5f36e198 --- /dev/null +++ b/examples/fastify-postgres/solarwinds.apm.config.js @@ -0,0 +1,21 @@ +/* +Copyright 2023 SolarWinds Worldwide, LLC. + +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. +*/ + +/** @type {import("solarwinds-apm").Config} */ +export default { + insertTraceContextIntoQueries: true, + insertTraceContextIntoLogs: true, +} diff --git a/examples/fastify-postgres/solarwinds.apm.config.json b/examples/fastify-postgres/solarwinds.apm.config.json deleted file mode 100644 index 3b08b42f..00000000 --- a/examples/fastify-postgres/solarwinds.apm.config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "insertTraceContextIntoQueries": true, - "insertTraceContextIntoLogs": true -} diff --git a/packages/instrumentations/COMPATIBILITY.md b/packages/instrumentations/COMPATIBILITY.md index f885ecda..e1766f5c 100644 --- a/packages/instrumentations/COMPATIBILITY.md +++ b/packages/instrumentations/COMPATIBILITY.md @@ -4,7 +4,7 @@ | --------------------------- | --------------------- | | `@aws-sdk/middleware-stack` | `>=3.1.0 <4.0.0` | | `@aws-sdk/smithy-client` | `>=3.1.0 <4.0.0` | -| `@cucumber/cucumber` | `>=8.0.0 <10.0.0` | +| `@cucumber/cucumber` | `>=8.0.0 <11.0.0` | | `@hapi/hapi` | `>=17.0.0 <21.0.0` | | `@nestjs/core` | `>=4.0.0` | | `@node-redis/client` | `>=1.0.0 <2.0.0` | @@ -32,7 +32,7 @@ | `koa` | `>=2.0.0 <3.0.0` | | `lru-memoizer` | `>=1.3.0 <3.0.0` | | `memcached` | `>=2.2.0` | -| `mongodb` | `>=3.3.0 <6.0.0` | +| `mongodb` | `>=3.3.0 <7.0.0` | | `mongoose` | `>=5.9.7 <7.0.0` | | `mysql` | `>=2.0.0 <3.0.0` | | `mysql2` | `>=1.4.2 <4.0.0` | diff --git a/packages/module/package.json b/packages/module/package.json index f8898713..a869af4e 100644 --- a/packages/module/package.json +++ b/packages/module/package.json @@ -18,6 +18,10 @@ ".": { "import": "./dist/es/index.js", "require": "./dist/cjs/index.js" + }, + "./load": { + "import": "./dist/es/load.es.js", + "require": "./dist/cjs/load.cjs.js" } }, "main": "./dist/cjs/index.js", diff --git a/packages/module/src/load.cjs.ts b/packages/module/src/load.cjs.ts new file mode 100644 index 00000000..4500b81a --- /dev/null +++ b/packages/module/src/load.cjs.ts @@ -0,0 +1,29 @@ +/* +Copyright 2023 SolarWinds Worldwide, LLC. + +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 function load(file: string): unknown { + /* eslint-disable-next-line @typescript-eslint/no-var-requires */ + const required: unknown = require(file) + + const fakeEsmDefaultExport = + typeof required === "object" && + required !== null && + "default" in required && + Object.keys(required).length === 1 + + if (fakeEsmDefaultExport) return required.default + else return required +} diff --git a/packages/module/src/load.es.ts b/packages/module/src/load.es.ts new file mode 100644 index 00000000..f6e112c6 --- /dev/null +++ b/packages/module/src/load.es.ts @@ -0,0 +1,21 @@ +/* +Copyright 2023 SolarWinds Worldwide, LLC. + +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 async function load(file: string): Promise { + const imported = (await import(file)) as object + if ("default" in imported) return imported.default + else return imported +} diff --git a/packages/solarwinds-apm/CONFIGURATION.md b/packages/solarwinds-apm/CONFIGURATION.md index 31749414..71298344 100644 --- a/packages/solarwinds-apm/CONFIGURATION.md +++ b/packages/solarwinds-apm/CONFIGURATION.md @@ -9,7 +9,7 @@ All configuration options are optional except for the service key which is alway When required, the package will look for the file in the current working directory under three possible formats, in the following order: - `solarwinds.apm.config.ts` - TypeScript config, supports all options, requires a TypeScript loader such as `ts-node` or `tsx` -- `solarwinds.apm.config.js` - JavaScript config, supports all options +- `solarwinds.apm.config.js` - JavaScript config, supports all options, `.cjs` extension also accepted - `solarwinds.apm.config.json` - JSON config, doesn't support certain options It's also possible to use a custom name for the configuration file using the `SW_APM_CONFIG_FILE` environment variable. The file must have one of the three supported extensions or it will be ignored. @@ -28,7 +28,7 @@ export default config ``` ```js -/** @type {import('solarwinds-apm').Config} */ +/** @type {import("solarwinds-apm").Config} */ module.exports = { // ... } diff --git a/packages/solarwinds-apm/src/config.ts b/packages/solarwinds-apm/src/config.ts index b3e6d9ae..329f1944 100644 --- a/packages/solarwinds-apm/src/config.ts +++ b/packages/solarwinds-apm/src/config.ts @@ -15,7 +15,6 @@ limitations under the License. */ import * as fs from "node:fs" -import { createRequire } from "node:module" import * as path from "node:path" import * as process from "node:process" @@ -25,14 +24,13 @@ import { InstrumentationBase } from "@opentelemetry/instrumentation" import { View } from "@opentelemetry/sdk-metrics" import { oboe } from "@solarwinds-apm/bindings" import { type InstrumentationConfigMap } from "@solarwinds-apm/instrumentations" -import { callsite, IS_SERVERLESS } from "@solarwinds-apm/module" +import { IS_SERVERLESS } from "@solarwinds-apm/module" +import { load } from "@solarwinds-apm/module/load" import { type SwConfiguration } from "@solarwinds-apm/sdk" import { z } from "zod" import aoCert from "./appoptics.crt.js" -const r = createRequire(callsite().getFileName()!) - const otelEnv = getEnvWithoutDefaults() const boolean = z.union([ @@ -188,36 +186,46 @@ const ENV_PREFIX = "SW_APM_" const ENV_PREFIX_DEV = `${ENV_PREFIX}DEV_` const DEFAULT_FILE_NAME = "solarwinds.apm.config" -export function readConfig(): ExtendedSwConfiguration { +export function readConfig(): + | ExtendedSwConfiguration + | Promise { const env = envObject() const devEnv = envObject(ENV_PREFIX_DEV) - const path = filePath() - const file = path ? readConfigFile(path) : {} + const processFile = (file: object): ExtendedSwConfiguration => { + const devFile = + "dev" in file && typeof file.dev === "object" && file.dev !== null + ? file.dev + : {} - const devFile = file.dev && typeof file.dev === "object" ? file.dev : {} + const raw = schema.parse({ + ...file, + ...env, + dev: { ...devFile, ...devEnv }, + }) - const raw = schema.parse({ - ...file, - ...env, - dev: { ...devFile, ...devEnv }, - }) + const config: ExtendedSwConfiguration = { + ...raw, + token: raw.serviceKey.token, + serviceName: raw.serviceKey.name, + certificate: raw.trustedpath, + oboeLogLevel: otelLevelToOboeLevel(raw.logLevel), + otelLogLevel: otelEnv.OTEL_LOG_LEVEL ?? raw.logLevel, + } - const config: ExtendedSwConfiguration = { - ...raw, - token: raw.serviceKey.token, - serviceName: raw.serviceKey.name, - certificate: raw.trustedpath, - oboeLogLevel: otelLevelToOboeLevel(raw.logLevel), - otelLogLevel: otelEnv.OTEL_LOG_LEVEL ?? raw.logLevel, - } + if (config.collector?.includes("appoptics.com")) { + config.metricFormat ??= 1 + config.certificate ??= aoCert + } - if (config.collector?.includes("appoptics.com")) { - config.metricFormat ??= 1 - config.certificate ??= aoCert + return config } - return config + const path = filePath() + const file = path ? readConfigFile(path) : {} + + if (file instanceof Promise) return file.then(processFile) + else return processFile(file) } export function printError(err: unknown) { @@ -294,23 +302,25 @@ function filePath() { return override } else { const fullName = path.join(cwd, DEFAULT_FILE_NAME) - const options = [`${fullName}.ts`, `${fullName}.js`, `${fullName}.json`] + const options = [ + `${fullName}.ts`, + `${fullName}.cjs`, + `${fullName}.js`, + `${fullName}.json`, + ] for (const option of options) { if (fs.existsSync(option)) return option } } } -function readConfigFile(path: string) { - const required = r(path) as Record - if ( - "default" in required && - (required.__esModule || Object.keys(required).length === 1) - ) { - return required.default as Record - } else { - return required +function readConfigFile(path: string): object | Promise { + if (path.endsWith(".json")) { + const contents = fs.readFileSync(path, { encoding: "utf-8" }) + return JSON.parse(contents) as object } + + return load(path) as object | Promise } function otelLevelToOboeLevel(level?: DiagLogLevel): number { diff --git a/packages/solarwinds-apm/src/init.ts b/packages/solarwinds-apm/src/init.ts index 4f26b778..a67e7110 100644 --- a/packages/solarwinds-apm/src/init.ts +++ b/packages/solarwinds-apm/src/init.ts @@ -74,7 +74,9 @@ export async function init() { let config: ExtendedSwConfiguration try { - config = readConfig() + const configOrPromise = readConfig() + if (configOrPromise instanceof Promise) config = await configOrPromise + else config = configOrPromise } catch (err) { console.warn( "Invalid SolarWinds APM configuration, application will not be instrumented",