Skip to content

Commit

Permalink
support ESM configs
Browse files Browse the repository at this point in the history
  • Loading branch information
raphael-theriault-swi committed Dec 15, 2023
1 parent f33bde9 commit 93fde63
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 44 deletions.
21 changes: 21 additions & 0 deletions examples/fastify-postgres/solarwinds.apm.config.js
Original file line number Diff line number Diff line change
@@ -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,
}
4 changes: 0 additions & 4 deletions examples/fastify-postgres/solarwinds.apm.config.json

This file was deleted.

4 changes: 2 additions & 2 deletions packages/instrumentations/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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` |
Expand Down
4 changes: 4 additions & 0 deletions packages/module/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 29 additions & 0 deletions packages/module/src/load.cjs.ts
Original file line number Diff line number Diff line change
@@ -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
}
21 changes: 21 additions & 0 deletions packages/module/src/load.es.ts
Original file line number Diff line number Diff line change
@@ -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<unknown> {
const imported = (await import(file)) as object
if ("default" in imported) return imported.default
else return imported
}
4 changes: 2 additions & 2 deletions packages/solarwinds-apm/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -28,7 +28,7 @@ export default config
```

```js
/** @type {import('solarwinds-apm').Config} */
/** @type {import("solarwinds-apm").Config} */
module.exports = {
// ...
}
Expand Down
80 changes: 45 additions & 35 deletions packages/solarwinds-apm/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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([
Expand Down Expand Up @@ -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<ExtendedSwConfiguration> {
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) {
Expand Down Expand Up @@ -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<string, unknown>
if (
"default" in required &&
(required.__esModule || Object.keys(required).length === 1)
) {
return required.default as Record<string, unknown>
} else {
return required
function readConfigFile(path: string): object | Promise<object> {
if (path.endsWith(".json")) {
const contents = fs.readFileSync(path, { encoding: "utf-8" })
return JSON.parse(contents) as object
}

return load(path) as object | Promise<object>
}

function otelLevelToOboeLevel(level?: DiagLogLevel): number {
Expand Down
4 changes: 3 additions & 1 deletion packages/solarwinds-apm/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 93fde63

Please sign in to comment.