From 29e018153d63d6d9067e5df356a8cbe73ecef483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Fri, 11 Oct 2024 21:36:30 -0400 Subject: [PATCH 1/9] back to working order --- .../solarwinds-apm/{version.js => build.js} | 3 + packages/solarwinds-apm/package.json | 18 +- packages/solarwinds-apm/src/api.ts | 22 +- .../src/appoptics/exporters/metrics.ts | 16 +- .../src/appoptics/exporters/traces.ts | 12 +- .../appoptics/processing/inbound-metrics.ts | 15 +- .../solarwinds-apm/src/appoptics/reporter.ts | 20 +- .../solarwinds-apm/src/appoptics/sampler.ts | 6 +- .../src/commonjs/.prettierrc.json | 4 + .../commonjs/api.d.ts} | 5 +- packages/solarwinds-apm/src/commonjs/api.js | 37 ++ .../solarwinds-apm/src/commonjs/index.d.ts | 18 + packages/solarwinds-apm/src/commonjs/index.js | 19 + .../solarwinds-apm/src/commonjs/package.json | 3 + .../solarwinds-apm/src/commonjs/version.d.ts | 18 + .../solarwinds-apm/src/commonjs/version.js | 71 +++ packages/solarwinds-apm/src/config.ts | 296 +++++------ packages/solarwinds-apm/src/exporters/logs.ts | 6 +- .../solarwinds-apm/src/exporters/metrics.ts | 6 +- .../solarwinds-apm/src/exporters/traces.ts | 6 +- .../src/{hooks.es.js => hooks.js} | 12 - packages/solarwinds-apm/src/index.cjs.ts | 41 -- .../src/{index.es.ts => index.ts} | 33 +- packages/solarwinds-apm/src/init.ts | 493 ++++++++---------- packages/solarwinds-apm/src/logger.ts | 13 +- packages/solarwinds-apm/src/patches.ts | 10 +- .../src/processing/parent-span.ts | 2 +- .../src/processing/response-time.ts | 8 +- .../src/processing/transaction-name.ts | 15 +- .../solarwinds-apm/src/propagation/headers.ts | 34 +- packages/solarwinds-apm/src/sampling/grpc.ts | 19 +- .../solarwinds-apm/src/sampling/sampler.ts | 6 +- packages/solarwinds-apm/src/storage.ts | 4 +- packages/solarwinds-apm/src/symbols.ts | 39 -- packages/solarwinds-apm/src/util.ts | 48 -- packages/solarwinds-apm/test/config.test.ts | 94 ++-- .../{test.config.cjs => configs/commonjs.cjs} | 0 .../transaction-settings.js} | 0 .../test/propagation/headers.test.ts | 38 ++ packages/solarwinds-apm/test/util.test.ts | 55 -- yarn.lock | 124 +---- 41 files changed, 758 insertions(+), 931 deletions(-) rename packages/solarwinds-apm/{version.js => build.js} (89%) create mode 100644 packages/solarwinds-apm/src/commonjs/.prettierrc.json rename packages/solarwinds-apm/{rollup.config.js => src/commonjs/api.d.ts} (88%) create mode 100644 packages/solarwinds-apm/src/commonjs/api.js create mode 100644 packages/solarwinds-apm/src/commonjs/index.d.ts create mode 100644 packages/solarwinds-apm/src/commonjs/index.js create mode 100644 packages/solarwinds-apm/src/commonjs/package.json create mode 100644 packages/solarwinds-apm/src/commonjs/version.d.ts create mode 100644 packages/solarwinds-apm/src/commonjs/version.js rename packages/solarwinds-apm/src/{hooks.es.js => hooks.js} (69%) delete mode 100644 packages/solarwinds-apm/src/index.cjs.ts rename packages/solarwinds-apm/src/{index.es.ts => index.ts} (54%) delete mode 100644 packages/solarwinds-apm/src/symbols.ts delete mode 100644 packages/solarwinds-apm/src/util.ts rename packages/solarwinds-apm/test/{test.config.cjs => configs/commonjs.cjs} (100%) rename packages/solarwinds-apm/test/{test.config.js => configs/transaction-settings.js} (100%) delete mode 100644 packages/solarwinds-apm/test/util.test.ts diff --git a/packages/solarwinds-apm/version.js b/packages/solarwinds-apm/build.js similarity index 89% rename from packages/solarwinds-apm/version.js rename to packages/solarwinds-apm/build.js index 2255ad49..32522cd8 100644 --- a/packages/solarwinds-apm/version.js +++ b/packages/solarwinds-apm/build.js @@ -32,3 +32,6 @@ const linted = await new ESLint({ fix: true }).lintText(formatted, { }) await fs.writeFile("src/version.ts", linted[0].output) + +await fs.mkdir("dist/commonjs", { recursive: true }) +await fs.cp("src/commonjs/package.json", "dist/commonjs/package.json") diff --git a/packages/solarwinds-apm/package.json b/packages/solarwinds-apm/package.json index adddd4b4..7de9f3e0 100644 --- a/packages/solarwinds-apm/package.json +++ b/packages/solarwinds-apm/package.json @@ -32,13 +32,11 @@ "type": "module", "exports": { ".": { - "import": "./dist/es/index.es.js" - }, - "./loader": { - "import": "./dist/es/hooks.es.js" + "import": "./dist/index.js", + "require": "./dist/commonjs/index.js" } }, - "main": "./dist/cjs/index.cjs.js", + "main": "./dist/commonjs/index.js", "files": [ "./src/", "./dist/", @@ -50,9 +48,9 @@ "provenance": true }, "scripts": { - "build": "node version.js && rollup -c --forceExit", - "lint": "node version.js && prettier --check . && eslint . --max-warnings=0", - "lint:fix": "node version.js && eslint --fix . && prettier --write .", + "build": "node build.js && tsc", + "lint": "node build.js && prettier --check . && eslint . --max-warnings=0", + "lint:fix": "node build.js && eslint --fix . && prettier --write .", "release": "node ../../scripts/publish.js", "test": "swtest -p test/tsconfig.json -c src" }, @@ -74,13 +72,11 @@ "@solarwinds-apm/dependencies": "workspace:^", "@solarwinds-apm/histogram": "workspace:^", "@solarwinds-apm/instrumentations": "workspace:^", - "@solarwinds-apm/lazy": "workspace:^", "@solarwinds-apm/module": "workspace:^", "@solarwinds-apm/proto": "workspace:^", "@solarwinds-apm/sampling": "workspace:^", - "@solarwinds-apm/sdk": "workspace:^", "json-stringify-safe": "^5.0.1", - "semver": "^7.5.4", + "node-releases": "^2.0.18", "zod": "^3.22.4" }, "peerDependencies": { diff --git a/packages/solarwinds-apm/src/api.ts b/packages/solarwinds-apm/src/api.ts index 0a66623e..7753db04 100644 --- a/packages/solarwinds-apm/src/api.ts +++ b/packages/solarwinds-apm/src/api.ts @@ -14,15 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { context } from "@opentelemetry/api" -import * as sdk from "@solarwinds-apm/sdk" +import { API } from "./init.js" -export function setTransactionName(name: string): boolean { - return sdk.setTransactionName(context.active(), name) -} -export function waitUntilReady(timeout: number): number { - return sdk.waitUntilReady(timeout) +/** + * Wait until the library is ready to sample traces + * + * Note that when exporting to AppOptics this function will block the event loop. + * + * @param timeout - Wait timeout in milliseconds + * @returns Whether the library is ready + */ +export async function waitUntilReady(timeout: number): Promise { + const api = await API.value + return api.waitUntilReady(timeout) } export { type Config } from "./config.js" -export { FULL_VERSION, VERSION } from "./version.js" +export { setTransactionName } from "./processing/transaction-name.js" +export { VERSION } from "./version.js" diff --git a/packages/solarwinds-apm/src/appoptics/exporters/metrics.ts b/packages/solarwinds-apm/src/appoptics/exporters/metrics.ts index f30ced37..25fcfb32 100644 --- a/packages/solarwinds-apm/src/appoptics/exporters/metrics.ts +++ b/packages/solarwinds-apm/src/appoptics/exporters/metrics.ts @@ -16,7 +16,7 @@ limitations under the License. import util from "node:util" -import { type Attributes, type DiagLogger } from "@opentelemetry/api" +import { type Attributes } from "@opentelemetry/api" import { type ExportResult, ExportResultCode, @@ -40,15 +40,15 @@ import { type Histogram, } from "@solarwinds-apm/histogram" +import { componentLogger } from "../../logger.js" + const MAX_TAGS = 50 export class AppopticsMetricExporter { + readonly #logger = componentLogger(AppopticsMetricExporter) readonly #reporter: oboe.Reporter - constructor( - reporter: oboe.Reporter, - protected readonly logger: DiagLogger, - ) { + constructor(reporter: oboe.Reporter) { this.#reporter = reporter } @@ -100,7 +100,7 @@ export class AppopticsMetricExporter { tagCount, ) } else { - this.logger.warn( + this.#logger.warn( "gauges with delta aggregation are not supported", ) } @@ -113,7 +113,7 @@ export class AppopticsMetricExporter { ) this.#exportHistogram(histogram, name, tags, tagCount) } else { - this.logger.warn( + this.#logger.warn( "histograms with cumulative aggregation are not supported", ) } @@ -126,7 +126,7 @@ export class AppopticsMetricExporter { ) this.#exportHistogram(histogram, name, tags, tagCount) } else { - this.logger.warn( + this.#logger.warn( "histograms with cumulative aggregation are not supported", ) } diff --git a/packages/solarwinds-apm/src/appoptics/exporters/traces.ts b/packages/solarwinds-apm/src/appoptics/exporters/traces.ts index a2a17cd5..473cc86a 100644 --- a/packages/solarwinds-apm/src/appoptics/exporters/traces.ts +++ b/packages/solarwinds-apm/src/appoptics/exporters/traces.ts @@ -18,7 +18,6 @@ import { inspect } from "node:util" import { type AttributeValue, - type DiagLogger, type SpanContext, SpanKind, SpanStatusCode, @@ -40,17 +39,16 @@ import { } from "@opentelemetry/semantic-conventions" import { oboe } from "@solarwinds-apm/bindings" +import { componentLogger } from "../../logger.js" import { TRANSACTION_NAME_ATTRIBUTE } from "../../processing/transaction-name.js" import { traceParent } from "../sampler.js" export class AppopticsTraceExporter implements SpanExporter { + readonly #logger = componentLogger(AppopticsTraceExporter) readonly #reporter: oboe.Reporter #error: Error | undefined = undefined - constructor( - reporter: oboe.Reporter, - protected readonly logger: DiagLogger, - ) { + constructor(reporter: oboe.Reporter) { this.#reporter = reporter } @@ -195,8 +193,8 @@ export class AppopticsTraceExporter implements SpanExporter { this.#error = new Error( `Reporter::sendReport returned with error status ${status}`, ) - this.logger.warn("error sending report", this.#error) - this.logger.debug(evt.metadataString()) + this.#logger.warn("error sending report", this.#error) + this.#logger.debug(evt.metadataString()) } } } diff --git a/packages/solarwinds-apm/src/appoptics/processing/inbound-metrics.ts b/packages/solarwinds-apm/src/appoptics/processing/inbound-metrics.ts index 2c86554e..8462ef16 100644 --- a/packages/solarwinds-apm/src/appoptics/processing/inbound-metrics.ts +++ b/packages/solarwinds-apm/src/appoptics/processing/inbound-metrics.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { type DiagLogger, SpanStatusCode } from "@opentelemetry/api" +import { SpanStatusCode } from "@opentelemetry/api" import { hrTimeToMicroseconds } from "@opentelemetry/core" import { NoopSpanProcessor, @@ -22,8 +22,9 @@ import { type SpanProcessor, } from "@opentelemetry/sdk-trace-base" import { oboe } from "@solarwinds-apm/bindings" -import { type SwConfiguration } from "@solarwinds-apm/sdk" +import { type Configuration } from "../../config.js" +import { componentLogger } from "../../logger.js" import { isRootOrEntry } from "../../processing/parent-span.js" import { computedTransactionName, @@ -35,12 +36,10 @@ export class AppopticsInboundMetricsProcessor extends NoopSpanProcessor implements SpanProcessor { + readonly #logger = componentLogger(AppopticsInboundMetricsProcessor) readonly #defaultTransactionName?: string - constructor( - config: SwConfiguration, - protected readonly logger: DiagLogger, - ) { + constructor(config: Configuration) { super() this.#defaultTransactionName = config.transactionName } @@ -55,7 +54,7 @@ export class AppopticsInboundMetricsProcessor const duration = hrTimeToMicroseconds(span.duration) let transaction = span.attributes[TRANSACTION_NAME_ATTRIBUTE] - this.logger.debug("initial transaction name", transaction) + this.#logger.debug("initial transaction name", transaction) if (typeof transaction !== "string") { transaction = this.#defaultTransactionName ?? computedTransactionName(span) @@ -79,7 +78,7 @@ export class AppopticsInboundMetricsProcessor domain: null, }) } - this.logger.debug("final transaction name", transaction) + this.#logger.debug("final transaction name", transaction) span.attributes[TRANSACTION_NAME_ATTRIBUTE] = transaction } diff --git a/packages/solarwinds-apm/src/appoptics/reporter.ts b/packages/solarwinds-apm/src/appoptics/reporter.ts index f007c68f..02b62632 100644 --- a/packages/solarwinds-apm/src/appoptics/reporter.ts +++ b/packages/solarwinds-apm/src/appoptics/reporter.ts @@ -28,27 +28,29 @@ import { ATTR_PROCESS_COMMAND_LINE, } from "@opentelemetry/semantic-conventions/incubating" import { oboe } from "@solarwinds-apm/bindings" -import { type SwConfiguration } from "@solarwinds-apm/sdk" +import { type Configuration } from "../config.js" import { modules } from "../metadata.js" import { VERSION } from "../version.js" import certificate from "./certificate.js" +export const ERROR: Error | undefined = oboe instanceof Error ? oboe : undefined + export async function reporter( - config: SwConfiguration, + config: Configuration, resource: Resource, ): Promise { const reporter = new oboe.Reporter({ - service_key: `${config.token}:${config.serviceName}`, - host: config.collector ?? "", - certificates: config.certificate ?? certificate, + service_key: `${config.serviceKey?.token}:${config.service}`, + host: config.collector, + certificates: config.trustedpath ?? certificate, grpc_proxy: config.proxy ?? "", reporter: "ssl", metric_format: 1, trace_metrics: 1, - log_level: otelLevelToOboeLevel(config.otelLogLevel), - log_type: otelLevelToOboeType(config.otelLogLevel), + log_level: otelLevelToOboeLevel(config.logLevel), + log_type: otelLevelToOboeType(config.logLevel), log_file_path: "", buffer_size: oboe.SETTINGS_UNSET, @@ -66,7 +68,7 @@ export async function reporter( }) const logger = diag.createComponentLogger({ - namespace: `[sw/oboe]`, + namespace: `[solarwinds-apm / oboe]`, }) oboe.debug_log_add((level, sourceName, sourceLine, message) => { const log = oboeLevelToOtelLogger(level, logger) @@ -79,7 +81,7 @@ export async function reporter( } else { log(message) } - }, config.oboeLogLevel) + }, otelLevelToOboeLevel(config.logLevel)) // Send init message const md = oboe.Metadata.makeRandom(true) diff --git a/packages/solarwinds-apm/src/appoptics/sampler.ts b/packages/solarwinds-apm/src/appoptics/sampler.ts index ba9a55de..b2394d31 100644 --- a/packages/solarwinds-apm/src/appoptics/sampler.ts +++ b/packages/solarwinds-apm/src/appoptics/sampler.ts @@ -108,7 +108,11 @@ export class AppopticsSampler extends Sampler { } override toString(): string { - return `Legacy Sampler` + return "AppOptics Sampler" + } + + isReady(timeout: number): boolean { + return oboe.Context.isReady(timeout) === oboe.SERVER_RESPONSE_OK } } diff --git a/packages/solarwinds-apm/src/commonjs/.prettierrc.json b/packages/solarwinds-apm/src/commonjs/.prettierrc.json new file mode 100644 index 00000000..b1734327 --- /dev/null +++ b/packages/solarwinds-apm/src/commonjs/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "semi": true, + "trailingComma": "none" +} diff --git a/packages/solarwinds-apm/rollup.config.js b/packages/solarwinds-apm/src/commonjs/api.d.ts similarity index 88% rename from packages/solarwinds-apm/rollup.config.js rename to packages/solarwinds-apm/src/commonjs/api.d.ts index fb0d311b..5f48b586 100644 --- a/packages/solarwinds-apm/rollup.config.js +++ b/packages/solarwinds-apm/src/commonjs/api.d.ts @@ -14,6 +14,5 @@ See the License for the specific language governing permissions and limitations under the License. */ -import config from "@solarwinds-apm/rollup-config" - -export default config() +import * as api from "../api.js"; +export = api; diff --git a/packages/solarwinds-apm/src/commonjs/api.js b/packages/solarwinds-apm/src/commonjs/api.js new file mode 100644 index 00000000..f57e2725 --- /dev/null +++ b/packages/solarwinds-apm/src/commonjs/api.js @@ -0,0 +1,37 @@ +/* +Copyright 2023-2024 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. +*/ + +module.exports.VERSION = "0.0.0-0"; + +module.exports.setTransactionName = function setTransactionName(name) { + if (api) { + return api.setTransactionName(name); + } else { + return false; + } +}; + +module.exports.waitUntilReady = function waitUntilReady(timeout) { + return imported.then((api) => api.waitUntilReady(timeout)); +}; + +/** @type{import("../api")} */ +let api = undefined; +const imported = import("../api.js").then((imported) => { + module.exports.VERSION = imported.VERSION; + api = imported; + return imported; +}); diff --git a/packages/solarwinds-apm/src/commonjs/index.d.ts b/packages/solarwinds-apm/src/commonjs/index.d.ts new file mode 100644 index 00000000..dbc811ae --- /dev/null +++ b/packages/solarwinds-apm/src/commonjs/index.d.ts @@ -0,0 +1,18 @@ +/* +Copyright 2023-2024 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. +*/ + +import api from "./api"; +export = api; diff --git a/packages/solarwinds-apm/src/commonjs/index.js b/packages/solarwinds-apm/src/commonjs/index.js new file mode 100644 index 00000000..959847ac --- /dev/null +++ b/packages/solarwinds-apm/src/commonjs/index.js @@ -0,0 +1,19 @@ +/* +Copyright 2023-2024 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. +*/ + +if (require("./version")) { + module.exports = require("./api"); +} diff --git a/packages/solarwinds-apm/src/commonjs/package.json b/packages/solarwinds-apm/src/commonjs/package.json new file mode 100644 index 00000000..5bbefffb --- /dev/null +++ b/packages/solarwinds-apm/src/commonjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/packages/solarwinds-apm/src/commonjs/version.d.ts b/packages/solarwinds-apm/src/commonjs/version.d.ts new file mode 100644 index 00000000..74171a20 --- /dev/null +++ b/packages/solarwinds-apm/src/commonjs/version.d.ts @@ -0,0 +1,18 @@ +/* +Copyright 2023-2024 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. +*/ + +declare const supported: boolean; +export = supported; diff --git a/packages/solarwinds-apm/src/commonjs/version.js b/packages/solarwinds-apm/src/commonjs/version.js new file mode 100644 index 00000000..72df9e62 --- /dev/null +++ b/packages/solarwinds-apm/src/commonjs/version.js @@ -0,0 +1,71 @@ +/* eslint-disable */ +"use strict"; + +var fs = require("fs"); +var path = require("path"); +var process = require("process"); + +try { + if ( + typeof process.version !== "string" || + process.version.substring(0, 2) === "v0" + ) { + console.error("UPDATE YOUR NODE.JS VERSION IMMEDIATELY."); + process.exit(1); + } + + var file = require.resolve( + "node-releases/data/release-schedule/release-schedule.json" + ); + var versions = JSON.parse(fs.readFileSync(file, { encoding: "utf8" })); + for (var major in versions) { + if (process.version.substring(0, major.length) !== major) { + continue; + } + + var eol = new Date(versions[major].end); + + var elapsed = Date.now() - eol.getTime(); + if (elapsed > 1000 * 60 * 60 * 24 * 365) { + var message = + "The detected Node.js version (" + + process.version + + ") has reached End Of Life over one year ago (" + + versions[major].end + + "). It is no longer supported by this library (solarwinds-apm) and things may break unexpectedly. " + + "SolarWinds STRONGLY recommends customers use a non-EOL Node.js version receiving security updates."; + + throw message; + } else if (elapsed > 0) { + var message = + "The detected Node.js version (" + + process.version + + ") has reached End Of Life (" + + versions[major].end + + "). It is still supported by this library (solarwinds-apm) for one year following this date. " + + "SolarWinds STRONGLY recommends customers use a non-EOL Node.js version receiving security updates."; + + throw message; + } + } + + module.exports = true; +} catch (error) { + console.warn(error); + module.exports = false; +} + +try { + var node = process.versions.node; + console.log("Node.js " + node); + + var solarwinds = JSON.parse( + fs.readFileSync(path.join(__dirname, "../../package.json"), { + encoding: "utf8" + }) + ).version; + console.log("solarwinds-apm " + solarwinds); + + var otel = require("@opentelemetry/core").VERSION; + console.log("@opentelemetry/core " + otel); +} catch (_error) {} diff --git a/packages/solarwinds-apm/src/config.ts b/packages/solarwinds-apm/src/config.ts index ec61761a..99dcb70e 100644 --- a/packages/solarwinds-apm/src/config.ts +++ b/packages/solarwinds-apm/src/config.ts @@ -14,23 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as fs from "node:fs" +import * as fs from "node:fs/promises" import * as path from "node:path" import * as process from "node:process" import { DiagLogLevel } from "@opentelemetry/api" import { getEnvWithoutDefaults } from "@opentelemetry/core" import { type Instrumentation } from "@opentelemetry/instrumentation" -import { View } from "@opentelemetry/sdk-metrics" -import { oboe } from "@solarwinds-apm/bindings" -import { type InstrumentationConfigMap } from "@solarwinds-apm/instrumentations" +import { type Detector, type DetectorSync } from "@opentelemetry/resources" +import { + type InstrumentationConfigMap, + type ResourceDetectorConfigMap, + type Set, +} from "@solarwinds-apm/instrumentations" import { IS_SERVERLESS } from "@solarwinds-apm/module" import { load } from "@solarwinds-apm/module/load" -import { type SwConfiguration } from "@solarwinds-apm/sdk" import { z, ZodError, ZodIssueCode } from "zod" -import aoCert from "./appoptics/certificate.js" - +const PREFIX = "SW_APM_" const ENDPOINTS = { traces: "/v1/traces", metrics: "/v1/metrics", @@ -72,7 +73,7 @@ const serviceKey = z const trustedpath = z.string().transform((p, ctx) => { try { - return fs.readFileSync(p, "utf-8") + return fs.readFile(p, "utf-8") } catch (err) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -128,20 +129,25 @@ const transactionSettings = z.array( })), ) +const set = z.enum(["none", "core", "all"]) + interface Instrumentations { configs?: InstrumentationConfigMap extra?: Instrumentation[] + set?: Set } -interface Metrics { - views: View[] - interval: number +interface ResourceDetectors { + configs?: ResourceDetectorConfigMap + extra?: (DetectorSync | Detector)[] + set?: Set } const schema = z.object({ serviceKey: serviceKey.optional(), enabled: boolean.default(true), - collector: z.string().optional(), + legacy: boolean.optional(), + collector: z.string().default("apm.collector.na-01.cloud.solarwinds.com"), trustedpath: trustedpath.optional(), proxy: z.string().optional(), logLevel: logLevel.default("warn"), @@ -157,37 +163,30 @@ const schema = z.object({ .object({ configs: z.record(z.unknown()).optional(), extra: z.array(z.unknown()).optional(), + set: set.default(IS_SERVERLESS ? "core" : "all"), }) .transform((i) => i as Instrumentations) .default({}), - metrics: z - .object({ - views: z.array(z.instanceof(View)).default([]), - interval: z.number().int().default(60_000), - }) - .default({}), - - dev: z + resourceDetectors: z .object({ - otlpTraces: boolean.default(IS_SERVERLESS), - otlpMetrics: boolean.default(IS_SERVERLESS), - swTraces: boolean.default(!IS_SERVERLESS), - swMetrics: boolean.default(!IS_SERVERLESS), - initMessage: boolean.default(!IS_SERVERLESS), - extraResourceDetection: boolean.default(!IS_SERVERLESS), - instrumentationsDefaultDisabled: boolean.default(IS_SERVERLESS), + configs: z.record(z.record(z.string(), z.boolean())).optional(), + extra: z.array(z.unknown()).optional(), + set: set.default(IS_SERVERLESS ? "core" : "all"), }) + .transform((i) => i as ResourceDetectors) .default({}), }) +/** User provided configuration for solarwinds-apm */ export interface Config extends z.input { instrumentations?: Instrumentations - metrics?: Metrics + resourceDetectors?: ResourceDetectors } -export interface ExtendedSwConfiguration extends SwConfiguration { - instrumentations: Instrumentations - metrics: Metrics +/** Processed configuration for solarwinds-apm */ +export interface Configuration extends z.output { + service: string + legacy: boolean otlp: { tracesEndpoint?: string @@ -195,99 +194,113 @@ export interface ExtendedSwConfiguration extends SwConfiguration { logsEndpoint?: string headers: Record } - dev: z.infer["dev"] source?: string } -const ENV_PREFIX = "SW_APM_" -const ENV_PREFIX_DEV = `${ENV_PREFIX}DEV_` -const DEFAULT_FILE_NAME = "solarwinds.apm.config" +export async function read(): Promise { + const paths: string[] = [] + if (typeof process.env.SW_APM_CONFIG_FILE === "string") { + paths.push(process.env.SW_APM_CONFIG_FILE) + } else { + paths.push( + "solarwinds.apm.config.ts", + "solarwinds.apm.config.mts", + "solarwinds.apm.config.cts", + "solarwinds.apm.config.js", + "solarwinds.apm.config.mjs", + "solarwinds.apm.config.cjs", + "solarwinds.apm.config.json", + ) + } + + const exists = (path: string) => + fs + .stat(path) + .then((stat) => stat.isFile()) + .catch(() => false) -export function readConfig(): - | ExtendedSwConfiguration - | Promise { const env = envObject() - const devEnv = envObject(ENV_PREFIX_DEV) + let file: object = {} + let source: string | undefined - const path = filePath() - const file = path ? readConfigFile(path) : {} + for (let option of paths) { + option = path.resolve(option) - const processFile = (file: object): ExtendedSwConfiguration => { - const devFile = - "dev" in file && typeof file.dev === "object" && file.dev !== null - ? file.dev - : {} + if (await exists(option)) { + try { + const read: unknown = + path.extname(option) === ".json" + ? JSON.parse(await fs.readFile(option, { encoding: "utf-8" })) + : await load(option) - const raw = schema.parse({ - ...file, - ...env, - dev: { ...devFile, ...devEnv }, - }) + if (typeof read !== "object" || read === null) { + throw new Error(`Expected config object, got ${typeof read}.`) + } - const otelEnv = getEnvWithoutDefaults() - - const serviceName = otelEnv.OTEL_SERVICE_NAME ?? raw.serviceKey?.name - if ( - !serviceName || - (!raw.serviceKey && (raw.dev.swTraces || raw.dev.swMetrics)) - ) { - throw new ZodError([ - { - path: ["serviceKey"], - message: "Missing service key", - code: ZodIssueCode.custom, - }, - ]) + file = read + source = option + } catch (error) { + console.warn(`The config file (${option}) could not be read.`, error) + } + } else if (paths.length === 1) { + console.warn(`The config file (${option}) could not be found.`) } + } - const config: ExtendedSwConfiguration = { - ...raw, - serviceName, - token: raw.serviceKey?.token ?? "", - certificate: raw.trustedpath, - oboeLogLevel: otelLevelToOboeLevel(raw.logLevel), - oboeLogType: otelLevelToOboeType(raw.logLevel), - otelLogLevel: otelEnv.OTEL_LOG_LEVEL ?? raw.logLevel, - source: path, - - otlp: { - tracesEndpoint: - otelEnv.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? - otelEnv.OTEL_EXPORTER_OTLP_ENDPOINT?.concat(ENDPOINTS.traces) ?? - raw.collector - ?.replace(/^apm\.collector\./, "https://otel.collector.") - .concat(ENDPOINTS.traces), - metricsEndpoint: - otelEnv.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT ?? - otelEnv.OTEL_EXPORTER_OTLP_ENDPOINT?.concat(ENDPOINTS.metrics) ?? - raw.collector - ?.replace(/^apm\.collector\./, "https://otel.collector.") - .concat(ENDPOINTS.metrics), - logsEndpoint: - otelEnv.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ?? - otelEnv.OTEL_EXPORTER_OTLP_ENDPOINT?.concat(ENDPOINTS.logs) ?? - raw.collector - ?.replace(/^apm\.collector\./, "https://otel.collector.") - .concat(ENDPOINTS.logs), - - headers: raw.serviceKey?.token - ? { authorization: `Bearer ${raw.serviceKey.token}` } - : {}, - }, - } + const raw = await schema.parseAsync({ ...file, ...env }) + const otel = getEnvWithoutDefaults() - if (config.collector?.includes("appoptics.com")) { - config.metricFormat ??= 1 - config.certificate ??= aoCert - config.exportLogsEnabled = false - } + const service = otel.OTEL_SERVICE_NAME ?? raw.serviceKey?.name + if (!service || (!IS_SERVERLESS && !raw.serviceKey?.token)) { + throw new ZodError([ + { + path: ["serviceKey"], + message: "Missing service key", + code: ZodIssueCode.custom, + }, + ]) + } - return config + const legacy = raw.legacy ?? raw.collector.includes("appoptics") + if (legacy && raw.exportLogsEnabled) { + console.warn("Logs export is not supported when exporting to AppOptics.") + raw.exportLogsEnabled = false } - if (file instanceof Promise) return file.then(processFile) - else return processFile(file) + return { + ...raw, + + service, + legacy, + + otlp: { + tracesEndpoint: + otel.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? + otel.OTEL_EXPORTER_OTLP_ENDPOINT?.concat(ENDPOINTS.traces) ?? + raw.collector + .replace(/^apm\.collector\./, "https://otel.collector.") + .concat(ENDPOINTS.traces), + metricsEndpoint: + otel.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT ?? + otel.OTEL_EXPORTER_OTLP_ENDPOINT?.concat(ENDPOINTS.metrics) ?? + raw.collector + .replace(/^apm\.collector\./, "https://otel.collector.") + .concat(ENDPOINTS.metrics), + logsEndpoint: + otel.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ?? + otel.OTEL_EXPORTER_OTLP_ENDPOINT?.concat(ENDPOINTS.logs) ?? + raw.collector + .replace(/^apm\.collector\./, "https://otel.collector.") + .concat(ENDPOINTS.logs), + + headers: raw.serviceKey?.token + ? { authorization: `Bearer ${raw.serviceKey.token}` } + : {}, + }, + + source, + } } export function printError(err: unknown) { @@ -329,82 +342,21 @@ export function printError(err: unknown) { } } -function fromEnvKey(k: string, prefix = ENV_PREFIX) { +function fromEnvKey(k: string, prefix = PREFIX) { return k .slice(prefix.length) .toLowerCase() .replace(/_[a-z]/g, (c) => c.slice(1).toUpperCase()) } -function toEnvKey(k: string, prefix = ENV_PREFIX) { +function toEnvKey(k: string, prefix = PREFIX) { return `${prefix}${k.replace(/[A-Z]/g, (c) => `_${c}`).toUpperCase()}` } -function envObject(prefix = ENV_PREFIX) { +function envObject(prefix = PREFIX) { return Object.fromEntries( Object.entries(process.env) .filter(([k]) => k.startsWith(prefix)) .map(([k, v]) => [fromEnvKey(k, prefix), v]), ) } - -function filePath() { - const cwd = process.cwd() - let override = process.env.SW_APM_CONFIG_FILE - - if (override) { - if (!path.isAbsolute(override)) { - override = path.join(cwd, override) - } - if (!fs.existsSync(override)) { - console.warn(`couldn't read config file at ${override}`) - return - } - - return override - } else { - const fullName = path.join(cwd, DEFAULT_FILE_NAME) - 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): 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 { - switch (level) { - case DiagLogLevel.NONE: - return oboe.INIT_LOG_LEVEL_FATAL - case DiagLogLevel.ERROR: - return oboe.INIT_LOG_LEVEL_ERROR - case DiagLogLevel.WARN: - return oboe.INIT_LOG_LEVEL_WARNING - case DiagLogLevel.INFO: - return oboe.INIT_LOG_LEVEL_INFO - case DiagLogLevel.DEBUG: - return oboe.INIT_LOG_LEVEL_DEBUG - case DiagLogLevel.VERBOSE: - case DiagLogLevel.ALL: - default: - return oboe.INIT_LOG_LEVEL_TRACE - } -} - -function otelLevelToOboeType(level: DiagLogLevel): number { - if (level === DiagLogLevel.NONE) return oboe.INIT_LOG_TYPE_DISABLE - else return oboe.INIT_LOG_TYPE_NULL -} diff --git a/packages/solarwinds-apm/src/exporters/logs.ts b/packages/solarwinds-apm/src/exporters/logs.ts index 90b9974a..74255f66 100644 --- a/packages/solarwinds-apm/src/exporters/logs.ts +++ b/packages/solarwinds-apm/src/exporters/logs.ts @@ -16,16 +16,16 @@ limitations under the License. import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto" -import { type ExtendedSwConfiguration } from "../config.js" +import { type Configuration } from "../config.js" export class LogExporter extends OTLPLogExporter { - constructor(config: ExtendedSwConfiguration) { + constructor(config: Configuration) { super({ url: config.otlp.logsEndpoint, headers: config.otlp.headers, // @ts-expect-error https://github.com/open-telemetry/opentelemetry-js/issues/5057 httpAgentOptions: { - ca: config.certificate, + ca: config.trustedpath, }, }) } diff --git a/packages/solarwinds-apm/src/exporters/metrics.ts b/packages/solarwinds-apm/src/exporters/metrics.ts index 0a576a8c..8011f960 100644 --- a/packages/solarwinds-apm/src/exporters/metrics.ts +++ b/packages/solarwinds-apm/src/exporters/metrics.ts @@ -22,15 +22,15 @@ import { InstrumentType, } from "@opentelemetry/sdk-metrics" -import { type ExtendedSwConfiguration } from "../config.js" +import { type Configuration } from "../config.js" export class MetricExporter extends OTLPMetricExporter { - constructor(config: ExtendedSwConfiguration) { + constructor(config: Configuration) { super({ url: config.otlp.metricsEndpoint, headers: config.otlp.headers, httpAgentOptions: { - ca: config.certificate, + ca: config.trustedpath, }, }) } diff --git a/packages/solarwinds-apm/src/exporters/traces.ts b/packages/solarwinds-apm/src/exporters/traces.ts index c2e7af90..c4f3016a 100644 --- a/packages/solarwinds-apm/src/exporters/traces.ts +++ b/packages/solarwinds-apm/src/exporters/traces.ts @@ -16,15 +16,15 @@ limitations under the License. import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto" -import { type ExtendedSwConfiguration } from "../config.js" +import { type Configuration } from "../config.js" export class TraceExporter extends OTLPTraceExporter { - constructor(config: ExtendedSwConfiguration) { + constructor(config: Configuration) { super({ url: config.otlp.tracesEndpoint, headers: config.otlp.headers, httpAgentOptions: { - ca: config.certificate, + ca: config.trustedpath, }, }) } diff --git a/packages/solarwinds-apm/src/hooks.es.js b/packages/solarwinds-apm/src/hooks.js similarity index 69% rename from packages/solarwinds-apm/src/hooks.es.js rename to packages/solarwinds-apm/src/hooks.js index dd39fce3..c5df8c44 100644 --- a/packages/solarwinds-apm/src/hooks.es.js +++ b/packages/solarwinds-apm/src/hooks.js @@ -14,16 +14,4 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { isMainThread } from "node:worker_threads" - -// init in here too so that everything can be done through a single --loader flag -if (isMainThread) { - try { - const { init } = await import("./init.js") - await init() - } catch (err) { - console.warn(err) - } -} - export * from "@opentelemetry/instrumentation/hook.mjs" diff --git a/packages/solarwinds-apm/src/index.cjs.ts b/packages/solarwinds-apm/src/index.cjs.ts deleted file mode 100644 index 41476536..00000000 --- a/packages/solarwinds-apm/src/index.cjs.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright 2023-2024 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. -*/ - -import { init } from "./init.js" -import { setter } from "./symbols.js" -import { versionCheck } from "./version.js" - -// init only once -const setInit = setter("init") -if (setInit && versionCheck()) { - setInit() - - console.warn( - "It looks like you're initialising solarwinds-apm using a require call or flag.", - "This initialisation method is not recommended and may be removed in a future release.", - "See https://github.com/solarwinds/apm-js/tree/main/packages/solarwinds-apm#installation-and-setup", - ) - - try { - init().catch((err: unknown) => { - console.warn(err) - }) - } catch (err) { - console.warn(err) - } -} - -export * from "./api.js" diff --git a/packages/solarwinds-apm/src/index.es.ts b/packages/solarwinds-apm/src/index.ts similarity index 54% rename from packages/solarwinds-apm/src/index.es.ts rename to packages/solarwinds-apm/src/index.ts index ade0b21f..13729c2a 100644 --- a/packages/solarwinds-apm/src/index.es.ts +++ b/packages/solarwinds-apm/src/index.ts @@ -14,34 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -import module from "node:module" -import process from "node:process" - -import semver from "semver" +import { IS_SERVERLESS } from "@solarwinds-apm/module" import { init } from "./init.js" -import { setter } from "./symbols.js" -import { versionCheck } from "./version.js" - -// init only once -const setInit = setter("init") -if (setInit && versionCheck()) { - setInit() - - // register only once - const setRegister = setter("register") - if ( - setRegister && - semver.satisfies(process.versions.node, "^18.19.0 || >=20.6.0") - ) { - setRegister() - module.register("./hooks.es.js", import.meta.url) - } +import { global } from "./storage.js" + +if (!IS_SERVERLESS) { + await import("./commonjs/version.js") +} +const first = Symbol() +if (global("init", () => first) === first) { try { await init() - } catch (err) { - console.warn(err) + } catch (error) { + console.error(error) } } diff --git a/packages/solarwinds-apm/src/init.ts b/packages/solarwinds-apm/src/init.ts index b6a572dc..fda165ca 100644 --- a/packages/solarwinds-apm/src/init.ts +++ b/packages/solarwinds-apm/src/init.ts @@ -14,363 +14,306 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { setTimeout } from "node:timers/promises" + import { diag, - type DiagLogFunction, type DiagLogger, metrics, - type TextMapPropagator, type TracerProvider, } from "@opentelemetry/api" -import { logs } from "@opentelemetry/api-logs" import { CompositePropagator, W3CBaggagePropagator } from "@opentelemetry/core" -import { - type Instrumentation, - registerInstrumentations, -} from "@opentelemetry/instrumentation" -import { Resource } from "@opentelemetry/resources" -import { - BatchLogRecordProcessor, - LoggerProvider, - type LogRecordProcessor, -} from "@opentelemetry/sdk-logs" +import { registerInstrumentations } from "@opentelemetry/instrumentation" +import { detectResourcesSync, Resource } from "@opentelemetry/resources" import { MeterProvider, type MetricReader, PeriodicExportingMetricReader, } from "@opentelemetry/sdk-metrics" -import { type Sampler, type SpanProcessor } from "@opentelemetry/sdk-trace-base" import { - NodeTracerProvider, - ParentBasedSampler, -} from "@opentelemetry/sdk-trace-node" -import { SEMRESATTRS_SERVICE_NAME } from "@opentelemetry/semantic-conventions" -import { oboe } from "@solarwinds-apm/bindings" + BatchSpanProcessor, + type Sampler, + type SpanProcessor, +} from "@opentelemetry/sdk-trace-base" +import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node" +import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions" +import { type oboe } from "@solarwinds-apm/bindings" import { - getDetectedResource, getInstrumentations, + getResource, } from "@solarwinds-apm/instrumentations" -import { IS_SERVERLESS } from "@solarwinds-apm/module" -import * as sdk from "@solarwinds-apm/sdk" +import { type AppopticsSampler } from "./appoptics/sampler.js" +import { type Configuration, printError, read } from "./config.js" +import { componentLogger, Logger } from "./logger.js" +import { patch } from "./patches.js" +import { ParentSpanProcessor } from "./processing/parent-span.js" +import { TransactionNameProcessor } from "./processing/transaction-name.js" import { - type ExtendedSwConfiguration, - printError, - readConfig, -} from "./config.js" -import { FULL_VERSION } from "./version.js" + RequestHeadersPropagator, + ResponseHeadersPropagator, +} from "./propagation/headers.js" +import { TraceContextPropagator } from "./propagation/trace-context.js" +import { type GrpcSampler } from "./sampling/grpc.js" +import { global } from "./storage.js" +import { VERSION } from "./version.js" + +interface Api { + waitUntilReady: (timeout: number) => Promise +} +export const API = global("api", () => { + const api: { value: Promise; resolve: (value: Api) => void } = { + value: null!, + resolve: null!, + } + api.value = new Promise((resolve) => (api.resolve = resolve)) + return api +}) export async function init() { - let config + let config: Configuration try { - config = readConfig() - if (config instanceof Promise) config = await config + config = await read() } catch (err) { console.warn( - "Invalid SolarWinds APM configuration, application will not be instrumented", + "Invalid SolarWinds APM configuration, application will not be instrumented.", ) printError(err) return } - diag.setLogger(new sdk.SwDiagLogger(), config.otelLogLevel) - const logger = diag.createComponentLogger({ namespace: "[sw/init]" }) - logger.debug(`CWD is ${process.cwd()}`) - logger.debug("SolarWinds APM configuration", config) + diag.setLogger(new Logger(), config.logLevel) + const logger = componentLogger(init) + logger.debug("working directory", process.cwd()) + logger.debug("config", config) if (!config.enabled) { - logger.info("Library disabled, application will not be instrumented") - return - } - if (sdk.OBOE_ERROR) { - logger.warn( - "Unsupported platform, application will not be instrumented", - sdk.OBOE_ERROR, - ) + logger.warn("Library disabled, application will not be instrumented.") return } - // initialize instrumentations before any asynchronous code or imports - let registerInstrumentations = initInstrumentations(config) - if (registerInstrumentations instanceof Promise) { - registerInstrumentations = await registerInstrumentations - } - - let resource = Resource.default().merge( - new Resource({ - [SEMRESATTRS_SERVICE_NAME]: config.serviceName, - "sw.data.module": "apm", - "sw.apm.version": FULL_VERSION, - }), - ) - resource = resource.merge( - await getDetectedResource(config.dev.extraResourceDetection), - ) - - const [reporter, serverlessApi] = IS_SERVERLESS - ? [undefined, sdk.createServerlessApi(config)] - : [sdk.createReporter(config), undefined] + const registerInstrumentations = await initInstrumentations(config, logger) + const resource = Resource.default() + .merge( + new Resource({ + [ATTR_SERVICE_NAME]: config.service, + "sw.data.module": "apm", + "sw.apm.version": VERSION, + }), + ) + .merge( + getResource( + config.resourceDetectors.configs ?? {}, + config.resourceDetectors.set!, + ), + ) + .merge( + detectResourcesSync({ + detectors: config.resourceDetectors.extra, + }), + ) - oboe.debug_log_add((level, sourceName, sourceLine, message) => { - const logger = diag.createComponentLogger({ - namespace: `[sw/oboe]`, - }) - const log = oboeLevelToOtelLogger(level, logger) + let oboe: oboe.Reporter | undefined + if (config.legacy) { + logger.debug("using oboe") - if (sourceName && level > oboe.INIT_LOG_LEVEL_INFO) { - const source = { source: sourceName, line: sourceLine } - log(message, source) - } else { - log(message) + const { reporter, ERROR } = await import("./appoptics/reporter.js") + if (ERROR) { + logger.warn( + "Unsupported platform for AppOptics, application will not be instrumented.", + ERROR, + ) + return } - }, config.oboeLogLevel) - const [tracerProvider, meterProvider] = await Promise.all([ - initTracing(config, resource, reporter, serverlessApi), - initMetrics(config, resource, logger, reporter), - initLogs(config, resource), - initMessage(config, resource, reporter), + oboe = await reporter(config, resource) + } + + const meterProvider = await initMetrics(config, resource, oboe, logger) + const [tracerProvider] = await Promise.all([ + initTracing(config, resource, oboe, logger), + initLogs(config, resource, logger), ]) + registerInstrumentations(tracerProvider, meterProvider) } -function initInstrumentations(config: ExtendedSwConfiguration) { - const traceOptionsResponsePropagator = - new sdk.SwTraceOptionsResponsePropagator() - - const registrer = (instrumentations: Instrumentation[]) => { - instrumentations = [ - ...instrumentations, - ...(config.instrumentations.extra ?? []), - ] +async function initInstrumentations(config: Configuration, logger: DiagLogger) { + logger.debug("initialising instrumentations") - return (tracerProvider: TracerProvider, meterProvider: MeterProvider) => - registerInstrumentations({ - instrumentations, - tracerProvider, - meterProvider, - }) - } - - const instrumentations = getInstrumentations( - sdk.patch(config.instrumentations.configs ?? {}, { + const provided = await getInstrumentations( + patch(config.instrumentations.configs ?? {}, { ...config, - responsePropagator: traceOptionsResponsePropagator, + responsePropagator: new ResponseHeadersPropagator(), }), - config.dev.instrumentationsDefaultDisabled, + config.instrumentations.set!, ) + const extra = config.instrumentations.extra ?? [] - if (instrumentations instanceof Promise) - return instrumentations.then(registrer) - else return registrer(instrumentations) + return (tracerProvider: TracerProvider, meterProvider: MeterProvider) => { + registerInstrumentations({ + instrumentations: [...provided, ...extra], + tracerProvider, + meterProvider, + }) + logger.debug("initialised instrumentations") + } } async function initTracing( - config: ExtendedSwConfiguration, + config: Configuration, resource: Resource, - reporter: oboe.Reporter | undefined, - serverlessApi: oboe.OboeAPI | undefined, + oboe: oboe.Reporter | undefined, + logger: DiagLogger, ) { + logger.debug("initialising tracing") + + let sampler: Sampler + let processors: SpanProcessor[] + const propagator = new CompositePropagator({ + propagators: [ + new RequestHeadersPropagator(), + new TraceContextPropagator(), + new W3CBaggagePropagator(), + ], + }) + + if (oboe) { + const [ + { AppopticsSampler }, + { AppopticsTraceExporter }, + { AppopticsInboundMetricsProcessor }, + ] = await Promise.all([ + import("./appoptics/sampler.js"), + import("./appoptics/exporters/traces.js"), + import("./appoptics/processing/inbound-metrics.js"), + ]) + + sampler = new AppopticsSampler( + config, + diag.createComponentLogger({ namespace: "[solarwinds-apm / sampler]" }), + ) + processors = [ + new AppopticsInboundMetricsProcessor(config), + new BatchSpanProcessor(new AppopticsTraceExporter(oboe)), + new ParentSpanProcessor(), + ] + + API.resolve({ + waitUntilReady: (timeout) => + Promise.resolve((sampler as AppopticsSampler).isReady(timeout)), + }) + } else { + const [{ GrpcSampler }, { TraceExporter }, { ResponseTimeProcessor }] = + await Promise.all([ + import("./sampling/grpc.js"), + import("./exporters/traces.js"), + import("./processing/response-time.js"), + ]) + + sampler = new GrpcSampler(config) + processors = [ + new TransactionNameProcessor(config), + new ResponseTimeProcessor(), + new BatchSpanProcessor(new TraceExporter(config)), + new ParentSpanProcessor(), + ] + + API.resolve({ + waitUntilReady: (timeout) => + new Promise((resolve) => { + void (sampler as GrpcSampler).ready.then(() => { + resolve(true) + }) + void setTimeout(timeout).then(() => { + resolve(false) + }) + }), + }) + } + const provider = new NodeTracerProvider({ - sampler: sampler(config, serverlessApi), + sampler, resource, }) - - const processors = await spanProcessors(config, reporter) for (const processor of processors) { provider.addSpanProcessor(processor) } + provider.register({ propagator }) - provider.register({ propagator: propagator() }) - + logger.debug("initialised tracing") return provider } async function initMetrics( - config: ExtendedSwConfiguration, + config: Configuration, resource: Resource, + oboe: oboe.Reporter | undefined, logger: DiagLogger, - reporter: oboe.Reporter | undefined, ) { - const readers = await metricReaders(config, reporter) + logger.debug("initialiing metrics") + + let readers: MetricReader[] + + if (oboe) { + const { AppopticsMetricExporter } = await import( + "./appoptics/exporters/metrics.js" + ) + readers = [ + new PeriodicExportingMetricReader({ + exporter: new AppopticsMetricExporter(oboe), + }), + ] + } else { + const { MetricExporter } = await import("./exporters/metrics.js") + readers = [ + new PeriodicExportingMetricReader({ + exporter: new MetricExporter(config), + }), + ] + } const provider = new MeterProvider({ resource, readers, - views: [...sdk.metrics.views, ...config.metrics.views], }) - metrics.setGlobalMeterProvider(provider) if (config.runtimeMetrics) { - if (sdk.METRICS_ERROR) { - logger.warn( - "Unsupported platform, runtime metrics will not be collected", - sdk.METRICS_ERROR, - ) - } else { - sdk.metrics.start() - } - } - - return provider -} - -async function initLogs(config: ExtendedSwConfiguration, resource: Resource) { - if (!config.exportLogsEnabled) return - - const provider = new LoggerProvider({ resource }) + logger.debug("initialising runtime metrics") - const processors = await logRecordProcessors(config) - for (const processor of processors) { - provider.addLogRecordProcessor(processor) + const { enable } = await import("./metrics/runtime.js") + enable() } - logs.setGlobalLoggerProvider(provider) - + logger.debug("initialised metrics") return provider } -async function initMessage( - config: ExtendedSwConfiguration, +async function initLogs( + config: Configuration, resource: Resource, - reporter: oboe.Reporter | undefined, + logger: DiagLogger, ) { - if (!config.dev.initMessage || !reporter) return - - if (resource.asyncAttributesPending) { - await resource.waitForAsyncAttributes?.() - } - sdk.sendStatus(reporter, await sdk.initMessage(resource, FULL_VERSION)) -} - -function sampler( - config: ExtendedSwConfiguration, - serverlessApi: oboe.OboeAPI | undefined, -): Sampler { - const sampler = new sdk.SwSampler( - config, - diag.createComponentLogger({ namespace: "[sw/sampler]" }), - serverlessApi, - ) - return new ParentBasedSampler({ - root: sampler, - remoteParentSampled: sampler, - remoteParentNotSampled: sampler, - }) -} + if (!config.exportLogsEnabled) return + logger.debug("initialising logs") + + const [ + { logs }, + { BatchLogRecordProcessor, LoggerProvider }, + { LogExporter }, + ] = await Promise.all([ + import("@opentelemetry/api-logs"), + import("@opentelemetry/sdk-logs"), + import("./exporters/logs.js"), + ]) -function propagator(): TextMapPropagator { - const baggagePropagator = new W3CBaggagePropagator() - const traceContextOptionsPropagator = new sdk.SwTraceContextOptionsPropagator( - diag.createComponentLogger({ namespace: "[sw/propagator]" }), + const provider = new LoggerProvider({ resource }) + provider.addLogRecordProcessor( + new BatchLogRecordProcessor(new LogExporter(config)), ) - return new CompositePropagator({ - propagators: [traceContextOptionsPropagator, baggagePropagator], - }) -} - -async function spanProcessors( - config: ExtendedSwConfiguration, - reporter: oboe.Reporter | undefined, -): Promise { - const processors: SpanProcessor[] = [] - const logger = diag.createComponentLogger({ namespace: "[sw/processor]" }) - - const parentInfoProcessor = new sdk.SwParentInfoSpanProcessor() - - if (config.dev.swTraces) { - const exporter = new sdk.SwExporter( - config, - reporter!, - diag.createComponentLogger({ namespace: "[sw/exporter]" }), - ) - const inboundMetricsProcessor = new sdk.SwInboundMetricsSpanProcessor() - processors.push( - new sdk.CompoundSpanProcessor( - exporter, - [parentInfoProcessor, inboundMetricsProcessor], - logger, - ), - ) - } - - if (config.dev.otlpTraces) { - const { TraceExporter } = await import("./exporters/traces.js") - const exporter = new TraceExporter(config) - - const responseTimeProcessor = new sdk.SwResponseTimeProcessor(config) - const transactionNameProcessor = new sdk.SwTransactionNameProcessor() - - processors.push( - new sdk.CompoundSpanProcessor( - exporter, - [parentInfoProcessor, responseTimeProcessor, transactionNameProcessor], - logger, - ), - ) - } - - return processors -} - -async function metricReaders( - config: ExtendedSwConfiguration, - reporter: oboe.Reporter | undefined, -): Promise { - const readers: MetricReader[] = [] - - if (config.dev.swMetrics) { - const exporter = new sdk.SwMetricsExporter( - reporter!, - diag.createComponentLogger({ namespace: "[sw/metrics]" }), - ) - readers.push( - new PeriodicExportingMetricReader({ - exporter, - exportIntervalMillis: config.metrics.interval, - }), - ) - } - - if (config.dev.otlpMetrics) { - const { MetricExporter } = await import("./exporters/metrics.js") - const exporter = new MetricExporter(config) - readers.push( - new PeriodicExportingMetricReader({ - exporter, - exportIntervalMillis: config.metrics.interval, - }), - ) - } - - return readers -} - -async function logRecordProcessors( - config: ExtendedSwConfiguration, -): Promise { - const { LogExporter } = await import("./exporters/logs.js") - return [new BatchLogRecordProcessor(new LogExporter(config))] -} + logs.setGlobalLoggerProvider(provider) -// https://github.com/boostorg/log/blob/boost-1.82.0/include/boost/log/trivial.hpp#L42-L50 -export function oboeLevelToOtelLogger( - level: number, - logger: DiagLogger, -): DiagLogFunction { - switch (level) { - case 0: - return logger.verbose.bind(logger) - case 1: - return logger.debug.bind(logger) - case 2: - return logger.info.bind(logger) - case 3: - return logger.warn.bind(logger) - case 4: - case 5: - default: - return logger.error.bind(logger) - } + logger.debug("logs initialised") + return provider } diff --git a/packages/solarwinds-apm/src/logger.ts b/packages/solarwinds-apm/src/logger.ts index 3dcaa887..faf54ba3 100644 --- a/packages/solarwinds-apm/src/logger.ts +++ b/packages/solarwinds-apm/src/logger.ts @@ -17,7 +17,7 @@ limitations under the License. import process from "node:process" import util from "node:util" -import { type DiagLogFunction, type DiagLogger } from "@opentelemetry/api" +import { diag, type DiagLogFunction, type DiagLogger } from "@opentelemetry/api" import stringify from "json-stringify-safe" const COLOURS = { @@ -26,6 +26,14 @@ const COLOURS = { cyan: "\x1b[1;36m", } +export function componentLogger(component: { + readonly name: string +}): DiagLogger { + return diag.createComponentLogger({ + namespace: `solarwinds-apm/${component.name}`, + }) +} + export class Logger implements DiagLogger { readonly error = Logger.makeLogger("error", "red") readonly warn = Logger.makeLogger("warn", "yellow") @@ -53,7 +61,8 @@ export class Logger implements DiagLogger { line += `) ${message}` for (const arg of args) { - const string = util.inspect(arg, false, Infinity, true) + const string = + typeof arg === "string" ? arg : util.inspect(arg, false, 4, true) line += ` ${string}` } diff --git a/packages/solarwinds-apm/src/patches.ts b/packages/solarwinds-apm/src/patches.ts index 91a71f9f..62a0414d 100644 --- a/packages/solarwinds-apm/src/patches.ts +++ b/packages/solarwinds-apm/src/patches.ts @@ -22,19 +22,21 @@ import { } from "@opentelemetry/api" import { type InstrumentationConfigMap } from "@solarwinds-apm/instrumentations" import { IS_AWS_LAMBDA } from "@solarwinds-apm/module" -import { type SwConfiguration } from "@solarwinds-apm/sdk" -export interface Options extends SwConfiguration { +import { type Configuration } from "./config.js" + +export interface Options extends Configuration { responsePropagator: TextMapPropagator } export function patch( configs: InstrumentationConfigMap, options: Options, -): void { +): InstrumentationConfigMap { for (const patcher of PATCHERS) { patcher(configs, options) } + return configs } function patcher( @@ -109,7 +111,7 @@ const PATCHERS = [ const original = config.logHook config.logHook = (span: Span, record: Record) => { - record["resource.service.name"] ??= options.serviceName + record["resource.service.name"] ??= options.service original?.(span, record) } diff --git a/packages/solarwinds-apm/src/processing/parent-span.ts b/packages/solarwinds-apm/src/processing/parent-span.ts index 8419f59c..da5fd915 100644 --- a/packages/solarwinds-apm/src/processing/parent-span.ts +++ b/packages/solarwinds-apm/src/processing/parent-span.ts @@ -25,7 +25,7 @@ import { import { spanStorage } from "../storage.js" /** Parent storage where false means no parent */ -const PARENT_STORAGE = spanStorage("solarwinds-apm parent span") +const PARENT_STORAGE = spanStorage("parent") /** Returns true if this span has no parent or its parent is remote */ export function isRootOrEntry(span: Span | sdk.Span | ReadableSpan): boolean { diff --git a/packages/solarwinds-apm/src/processing/response-time.ts b/packages/solarwinds-apm/src/processing/response-time.ts index f9c2c4dc..841a3926 100644 --- a/packages/solarwinds-apm/src/processing/response-time.ts +++ b/packages/solarwinds-apm/src/processing/response-time.ts @@ -16,7 +16,6 @@ limitations under the License. import { type Attributes, - type DiagLogger, metrics, SpanKind, SpanStatusCode, @@ -34,6 +33,7 @@ import { } from "@opentelemetry/semantic-conventions" import { lazy } from "@solarwinds-apm/lazy" +import { componentLogger } from "../logger.js" import { ATTR_HTTP_METHOD, ATTR_HTTP_STATUS_CODE } from "../semattrs.old.js" import { isRootOrEntry } from "./parent-span.js" import { TRANSACTION_NAME_ATTRIBUTE } from "./transaction-name.js" @@ -58,9 +58,7 @@ export class ResponseTimeProcessor extends NoopSpanProcessor implements SpanProcessor { - constructor(protected readonly logger: DiagLogger) { - super() - } + readonly #logger = componentLogger(ResponseTimeProcessor) override onEnd(span: ReadableSpan): void { if (!isRootOrEntry(span)) { @@ -87,7 +85,7 @@ export class ResponseTimeProcessor } } - this.logger.debug("recording response time", time, attributes) + this.#logger.debug("recording response time", time, attributes) RESPONSE_TIME.record(time, attributes) } } diff --git a/packages/solarwinds-apm/src/processing/transaction-name.ts b/packages/solarwinds-apm/src/processing/transaction-name.ts index 1b65fedf..11d3373c 100644 --- a/packages/solarwinds-apm/src/processing/transaction-name.ts +++ b/packages/solarwinds-apm/src/processing/transaction-name.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { type DiagLogger, trace } from "@opentelemetry/api" +import { trace } from "@opentelemetry/api" import { NoopSpanProcessor, type ReadableSpan, @@ -24,8 +24,9 @@ import { ATTR_HTTP_ROUTE, ATTR_URL_PATH, } from "@opentelemetry/semantic-conventions" -import { type SwConfiguration } from "@solarwinds-apm/sdk" +import { type Configuration } from "../config.js" +import { componentLogger } from "../logger.js" import { ATTR_HTTP_TARGET } from "../semattrs.old.js" import { getRootOrEntry, isRootOrEntry } from "./parent-span.js" @@ -62,6 +63,7 @@ export class TransactionNameProcessor extends NoopSpanProcessor implements SpanProcessor { + readonly #logger = componentLogger(TransactionNameProcessor) readonly #pool = new TransactionNamePool({ max: TRANSACTION_NAME_POOL_MAX, ttl: TRANSACTION_NAME_POOL_TTL, @@ -70,10 +72,7 @@ export class TransactionNameProcessor }) readonly #defaultName?: string - constructor( - config: SwConfiguration, - protected readonly logger: DiagLogger, - ) { + constructor(config: Configuration) { super() this.#defaultName = config.transactionName } @@ -84,12 +83,12 @@ export class TransactionNameProcessor } let name = span.attributes[TRANSACTION_NAME_ATTRIBUTE] - this.logger.debug("initial transaction name", name) + this.#logger.debug("initial transaction name", name, span.attributes) if (typeof name !== "string") { name = this.#defaultName ?? computedTransactionName(span) } name = this.#pool.registered(name) - this.logger.debug("final transaction name", name) + this.#logger.debug("final transaction name", name) span.attributes[TRANSACTION_NAME_ATTRIBUTE] = name } diff --git a/packages/solarwinds-apm/src/propagation/headers.ts b/packages/solarwinds-apm/src/propagation/headers.ts index 2f3ba42c..b1c7b9a8 100644 --- a/packages/solarwinds-apm/src/propagation/headers.ts +++ b/packages/solarwinds-apm/src/propagation/headers.ts @@ -23,7 +23,6 @@ import { import type * as sampling from "@solarwinds-apm/sampling" import { contextStorage } from "../storage.js" -import { firstIfArray, joinIfArray } from "../util.js" /** SolarWinds headers */ export interface Headers { @@ -105,3 +104,36 @@ export class ResponseHeadersPropagator implements TextMapPropagator { return context } } + +/** + * Returns the first element if the value is an array + * or the value as-is otherwise + */ +export function firstIfArray(value: T | T[] | undefined): T | undefined { + if (Array.isArray(value)) { + return value[0] + } else { + return value + } +} + +/** + * Returns the result of {@link Array.join} if the value is an array + * or the value as-is otherwise + * + * @param separator - Separator used between elements + */ +export function joinIfArray( + value: string | string[] | undefined, + separator: string, +): string | undefined { + if (Array.isArray(value)) { + if (value.length > 0) { + return value.join(separator) + } else { + return undefined + } + } else { + return value + } +} diff --git a/packages/solarwinds-apm/src/sampling/grpc.ts b/packages/solarwinds-apm/src/sampling/grpc.ts index 2a523ecf..77f44b11 100644 --- a/packages/solarwinds-apm/src/sampling/grpc.ts +++ b/packages/solarwinds-apm/src/sampling/grpc.ts @@ -19,7 +19,7 @@ import { hostname } from "node:os" import { TextDecoder } from "node:util" import { type CallOptions, Client, credentials, Metadata } from "@grpc/grpc-js" -import { context, type DiagLogger } from "@opentelemetry/api" +import { context } from "@opentelemetry/api" import { suppressTracing } from "@opentelemetry/core" import { collector } from "@solarwinds-apm/proto" import { @@ -29,9 +29,10 @@ import { SampleSource, type Settings, } from "@solarwinds-apm/sampling" -import { type SwConfiguration } from "@solarwinds-apm/sdk" import { Backoff } from "../backoff.js" +import { type Configuration } from "../config.js" +import { componentLogger } from "../logger.js" import { Sampler } from "./sampler.js" const CLIENT_VERSION = "2" @@ -77,13 +78,13 @@ export class GrpcSampler extends Sampler { readonly ready: Promise #ready!: () => void - constructor(config: SwConfiguration, logger: DiagLogger) { - super(config, logger) + constructor(config: Configuration) { + super(config, componentLogger(GrpcSampler)) - this.#key = `${config.token}:${config.serviceName}` + this.#key = `${config.serviceKey?.token}:${config.service}` // convert the collector string into a valid full URL - let collector = config.collector! + let collector = config.collector if (!/:{0-9}+$/.test(collector)) { collector = `${collector}:443` } @@ -94,7 +95,7 @@ export class GrpcSampler extends Sampler { const invalidCollectorClient = (cause?: unknown): CollectorClient => ({ getSettings: () => Promise.reject( - new Error(`Invalid collector "${config.collector!}"`, { cause }), + new Error(`Invalid collector "${config.collector}"`, { cause }), ), }) @@ -109,8 +110,8 @@ export class GrpcSampler extends Sampler { dns.resolve6(this.#address.hostname), ]) .then(() => { - const cred = config.certificate - ? credentials.createSsl(Buffer.from(config.certificate)) + const cred = config.trustedpath + ? credentials.createSsl(Buffer.from(config.trustedpath)) : credentials.createSsl() this.#client = new GrpcCollectorClient(this.#address.host, cred) }) diff --git a/packages/solarwinds-apm/src/sampling/sampler.ts b/packages/solarwinds-apm/src/sampling/sampler.ts index 14ead229..e4b5d759 100644 --- a/packages/solarwinds-apm/src/sampling/sampler.ts +++ b/packages/solarwinds-apm/src/sampling/sampler.ts @@ -35,8 +35,8 @@ import { type ResponseHeaders, TracingMode, } from "@solarwinds-apm/sampling" -import { type SwConfiguration } from "@solarwinds-apm/sdk" +import { type Configuration } from "../config.js" import { HEADERS_STORAGE } from "../propagation/headers.js" import { ATTR_HTTP_METHOD, @@ -104,9 +104,9 @@ export function httpSpanMetadata(kind: SpanKind, attributes: Attributes) { export abstract class Sampler extends OboeSampler { readonly #tracingMode: TracingMode | undefined readonly #triggerMode: boolean - readonly #transactionSettings: SwConfiguration["transactionSettings"] + readonly #transactionSettings: Configuration["transactionSettings"] - constructor(config: SwConfiguration, logger: DiagLogger) { + constructor(config: Configuration, logger: DiagLogger) { super(logger) if (config.tracingMode !== undefined) { diff --git a/packages/solarwinds-apm/src/storage.ts b/packages/solarwinds-apm/src/storage.ts index 998c0d5d..8914793b 100644 --- a/packages/solarwinds-apm/src/storage.ts +++ b/packages/solarwinds-apm/src/storage.ts @@ -31,7 +31,7 @@ import * as sdk from "@opentelemetry/sdk-trace-base" * @returns Shared global */ export function global(id: string, init: () => T): T { - const key = Symbol.for(`solarwinds-apm global storage / ${id}`) + const key = Symbol.for(`solarwinds-apm / ${id}`) let storage = Reflect.get(globalThis, key) as T | undefined if (!storage) { @@ -101,7 +101,7 @@ export interface SpanStorage { } const GLOBAL_SPAN_STORAGE = global( - "solarwinds-apm span storage", + "span storage", () => new WeakMap>(), ) diff --git a/packages/solarwinds-apm/src/symbols.ts b/packages/solarwinds-apm/src/symbols.ts deleted file mode 100644 index 7c804878..00000000 --- a/packages/solarwinds-apm/src/symbols.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2023-2024 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. -*/ - -import { name, version } from "../package.json" - -const ID = `${name}@${version}` - -/** - * Checks if a global flag is set. If it isn't returns a setter function for it, - * otherwise returns false. - * - * @param id - Unique ID to check and set - * @returns Setter function or false if already set - */ -export function setter(id: string): ((value?: unknown) => void) | false { - const symbol = Symbol.for(`${ID}/${id}`) - if (symbol in globalThis) return false - - return (value = true) => - Object.defineProperty(globalThis, symbol, { - value, - writable: false, - enumerable: false, - configurable: false, - }) -} diff --git a/packages/solarwinds-apm/src/util.ts b/packages/solarwinds-apm/src/util.ts deleted file mode 100644 index 7e239cea..00000000 --- a/packages/solarwinds-apm/src/util.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2023-2024 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. -*/ - -/** - * Returns the first element if the value is an array - * or the value as-is otherwise - */ -export function firstIfArray(value: T | T[] | undefined): T | undefined { - if (Array.isArray(value)) { - return value[0] - } else { - return value - } -} - -/** - * Returns the result of {@link Array.join} if the value is an array - * or the value as-is otherwise - * - * @param separator - Separator used between elements - */ -export function joinIfArray( - value: string | string[] | undefined, - separator: string, -): string | undefined { - if (Array.isArray(value)) { - if (value.length > 0) { - return value.join(separator) - } else { - return undefined - } - } else { - return value - } -} diff --git a/packages/solarwinds-apm/test/config.test.ts b/packages/solarwinds-apm/test/config.test.ts index cbd19d1f..044249d2 100644 --- a/packages/solarwinds-apm/test/config.test.ts +++ b/packages/solarwinds-apm/test/config.test.ts @@ -15,13 +15,11 @@ limitations under the License. */ import { DiagLogLevel } from "@opentelemetry/api" -import { oboe } from "@solarwinds-apm/bindings" import { beforeEach, describe, expect, it } from "@solarwinds-apm/test" -import aoCert from "../src/appoptics/certificate.js" -import { type ExtendedSwConfiguration, readConfig } from "../src/config.js" +import { type Configuration, read } from "../src/config.js" -describe("readConfig", () => { +describe("read", () => { beforeEach(() => { for (const key of Object.keys(process.env)) { if (key.startsWith("SW_APM_") || key.startsWith("OTEL_")) { @@ -32,32 +30,32 @@ describe("readConfig", () => { }) it("returns proper defaults", async () => { - const config = await readConfig() - const expected: ExtendedSwConfiguration = { - token: "token", - serviceName: "name", + const config = await read() + const expected: Configuration = { + service: "name", + serviceKey: { + name: "name", + token: "token", + }, enabled: true, - otelLogLevel: DiagLogLevel.WARN, - oboeLogLevel: oboe.INIT_LOG_LEVEL_WARNING, - oboeLogType: oboe.INIT_LOG_TYPE_NULL, + legacy: false, + collector: "apm.collector.na-01.cloud.solarwinds.com", + logLevel: DiagLogLevel.WARN, triggerTraceEnabled: true, runtimeMetrics: true, insertTraceContextIntoLogs: false, insertTraceContextIntoQueries: false, exportLogsEnabled: false, - instrumentations: {}, - metrics: { interval: 60_000, views: [] }, + instrumentations: { set: "all" }, + resourceDetectors: { set: "all" }, otlp: { + tracesEndpoint: + "https://otel.collector.na-01.cloud.solarwinds.com/v1/traces", + metricsEndpoint: + "https://otel.collector.na-01.cloud.solarwinds.com/v1/metrics", headers: { authorization: "Bearer token" }, - }, - dev: { - otlpTraces: false, - otlpMetrics: false, - swTraces: true, - swMetrics: true, - initMessage: true, - extraResourceDetection: true, - instrumentationsDefaultDisabled: false, + logsEndpoint: + "https://otel.collector.na-01.cloud.solarwinds.com/v1/logs", }, } @@ -67,7 +65,7 @@ describe("readConfig", () => { it("properly sets OTLP endpoints", async () => { process.env.SW_APM_COLLECTOR = "apm.collector.na-01.cloud.solarwinds.com" - const config = await readConfig() + const config = await read() expect(config.otlp).to.include({ tracesEndpoint: "https://otel.collector.na-01.cloud.solarwinds.com/v1/traces", @@ -80,90 +78,78 @@ describe("readConfig", () => { it("parses booleans", async () => { process.env.SW_APM_ENABLED = "0" - const config = await readConfig() + const config = await read() expect(config).to.include({ enabled: false }) }) it("parses tracing mode", async () => { process.env.SW_APM_TRACING_MODE = "enabled" - const config = await readConfig() + const config = await read() expect(config).to.include({ tracingMode: true }) }) it("parses trusted path", async () => { process.env.SW_APM_TRUSTEDPATH = "package.json" - const config = await readConfig() - expect(config.certificate).to.include("solarwinds-apm") + const config = await read() + expect(config.trustedpath).to.include("solarwinds-apm") }) it("parses transaction settings", async () => { - process.env.SW_APM_CONFIG_FILE = "test/test.config.js" + process.env.SW_APM_CONFIG_FILE = "test/configs/transaction-settings.js" - const config = await readConfig() + const config = await read() expect(config.transactionSettings).not.to.be.undefined expect(config.transactionSettings).to.have.length(3) }) - it("parses dev env", async () => { - process.env.SW_APM_DEV_OTLP_TRACES = "true" - process.env.SW_APM_DEV_SW_METRICS = "0" - - const config = await readConfig() - expect(config.dev.otlpTraces).to.be.true - expect(config.dev.swMetrics).to.be.false - }) - it("parses otel service name", async () => { process.env.OTEL_SERVICE_NAME = "otel-name" - const config = await readConfig() - expect(config.serviceName).to.equal("otel-name") + const config = await read() + expect(config.service).to.equal("otel-name") }) it("properly disables logging", async () => { process.env.SW_APM_LOG_LEVEL = "none" - const config = await readConfig() - expect(config.otelLogLevel).to.equal(DiagLogLevel.NONE) - expect(config.oboeLogType).to.equal(oboe.INIT_LOG_TYPE_DISABLE) + const config = await read() + expect(config.logLevel).to.equal(DiagLogLevel.NONE) }) - it("throws on bad boolean", () => { + it("throws on bad boolean", async () => { process.env.SW_APM_ENABLED = "foo" - expect(readConfig).to.throw() + await expect(read()).to.be.rejected }) - it("throws on bad tracing mode", () => { + it("throws on bad tracing mode", async () => { process.env.SW_APM_TRACING_MODE = "foo" - expect(readConfig).to.throw() + await expect(read()).to.be.rejected }) - it("throws on non-existent trusted path", () => { + it("throws on non-existent trusted path", async () => { process.env.SW_APM_TRUSTEDPATH = "foo" - expect(readConfig).to.throw() + await expect(read()).to.be.rejected }) it("uses the right defaults for AppOptics", async () => { process.env.SW_APM_COLLECTOR = "collector.appoptics.com" process.env.SW_APM_EXPORT_LOGS_ENABLED = "true" - const config = await readConfig() + const config = await read() expect(config).to.include({ - metricFormat: 1, - certificate: aoCert, exportLogsEnabled: false, }) }) it("supports cjs configs", async () => { - process.env.SW_APM_CONFIG_FILE = "test/test.config.cjs" + process.env.SW_APM_CONFIG_FILE = "test/configs/commonjs.cjs" - const config = await readConfig() + const config = await read() expect(config.transactionName).not.to.be.undefined expect(config.transactionName).to.equal("cjs") }) diff --git a/packages/solarwinds-apm/test/test.config.cjs b/packages/solarwinds-apm/test/configs/commonjs.cjs similarity index 100% rename from packages/solarwinds-apm/test/test.config.cjs rename to packages/solarwinds-apm/test/configs/commonjs.cjs diff --git a/packages/solarwinds-apm/test/test.config.js b/packages/solarwinds-apm/test/configs/transaction-settings.js similarity index 100% rename from packages/solarwinds-apm/test/test.config.js rename to packages/solarwinds-apm/test/configs/transaction-settings.js diff --git a/packages/solarwinds-apm/test/propagation/headers.test.ts b/packages/solarwinds-apm/test/propagation/headers.test.ts index 042b8218..831c211b 100644 --- a/packages/solarwinds-apm/test/propagation/headers.test.ts +++ b/packages/solarwinds-apm/test/propagation/headers.test.ts @@ -22,7 +22,9 @@ import { import { describe, expect, it } from "@solarwinds-apm/test" import { + firstIfArray, HEADERS_STORAGE, + joinIfArray, RequestHeadersPropagator, ResponseHeadersPropagator, } from "../../src/propagation/headers.js" @@ -94,3 +96,39 @@ describe("ResponseHeadersPropagator", () => { expect(propagator.fields()).to.have.members(["X-Trace-Options-Response"]) }) }) + +describe("firstIfArray", () => { + it("returns undefined if undefined", () => { + expect(firstIfArray(undefined)).to.be.undefined + }) + + it("returns value if not array", () => { + expect(firstIfArray("value")).to.equal("value") + }) + + it("returns undefined if empty array", () => { + expect(firstIfArray([])).to.be.undefined + }) + + it("returns first element if array", () => { + expect(firstIfArray([1, 2])).to.equal(1) + }) +}) + +describe("joinIfArray", () => { + it("returns undefined if undefined", () => { + expect(joinIfArray(undefined, ";")).to.be.undefined + }) + + it("returns value if not array", () => { + expect(joinIfArray("value", ";")).to.equal("value") + }) + + it("returns undefined if empty array", () => { + expect(joinIfArray([], ";")).to.be.undefined + }) + + it("returns joined if array", () => { + expect(joinIfArray(["foo", "bar"], ";")).to.equal("foo;bar") + }) +}) diff --git a/packages/solarwinds-apm/test/util.test.ts b/packages/solarwinds-apm/test/util.test.ts deleted file mode 100644 index f51b1628..00000000 --- a/packages/solarwinds-apm/test/util.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright 2023-2024 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. -*/ - -import { describe, expect, it } from "@solarwinds-apm/test" - -import { firstIfArray, joinIfArray } from "../src/util.js" - -describe("firstIfArray", () => { - it("returns undefined if undefined", () => { - expect(firstIfArray(undefined)).to.be.undefined - }) - - it("returns value if not array", () => { - expect(firstIfArray("value")).to.equal("value") - }) - - it("returns undefined if empty array", () => { - expect(firstIfArray([])).to.be.undefined - }) - - it("returns first element if array", () => { - expect(firstIfArray([1, 2])).to.equal(1) - }) -}) - -describe("joinIfArray", () => { - it("returns undefined if undefined", () => { - expect(joinIfArray(undefined, ";")).to.be.undefined - }) - - it("returns value if not array", () => { - expect(joinIfArray("value", ";")).to.equal("value") - }) - - it("returns undefined if empty array", () => { - expect(joinIfArray([], ";")).to.be.undefined - }) - - it("returns joined if array", () => { - expect(joinIfArray(["foo", "bar"], ";")).to.equal("foo;bar") - }) -}) diff --git a/yarn.lock b/yarn.lock index 5c6c84c4..6cdc0e56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1279,21 +1279,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/resource-detector-gcp@npm:^0.29.11": - version: 0.29.11 - resolution: "@opentelemetry/resource-detector-gcp@npm:0.29.11" - dependencies: - "@opentelemetry/core": "npm:^1.0.0" - "@opentelemetry/resources": "npm:^1.0.0" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - gcp-metadata: "npm:^6.0.0" - peerDependencies: - "@opentelemetry/api": ^1.0.0 - checksum: 10c0/cbaa9c384c6dfd08d861be6f5699fe75423452ac3651a540a8b493252e32788b2238df178e071b517159e816aae6b73ef7da54cfbe7e4489c78b97e9455d5243 - languageName: node - linkType: hard - -"@opentelemetry/resources@npm:1.26.0, @opentelemetry/resources@npm:^1.0.0, @opentelemetry/resources@npm:^1.10.0, @opentelemetry/resources@npm:^1.10.1, @opentelemetry/resources@npm:^1.8.0, @opentelemetry/resources@npm:~1.26.0": +"@opentelemetry/resources@npm:1.26.0, @opentelemetry/resources@npm:^1.10.0, @opentelemetry/resources@npm:^1.10.1, @opentelemetry/resources@npm:^1.8.0, @opentelemetry/resources@npm:~1.26.0": version: 1.26.0 resolution: "@opentelemetry/resources@npm:1.26.0" dependencies: @@ -1987,10 +1973,10 @@ __metadata: "@opentelemetry/resource-detector-aws": "npm:^1.3.1" "@opentelemetry/resource-detector-azure": "npm:^0.2.4" "@opentelemetry/resource-detector-container": "npm:^0.4.0" - "@opentelemetry/resource-detector-gcp": "npm:^0.29.11" "@opentelemetry/resources": "npm:~1.26.0" "@opentelemetry/winston-transport": "npm:^0.6.0" "@solarwinds-apm/eslint-config": "workspace:^" + "@solarwinds-apm/module": "workspace:^" "@solarwinds-apm/rollup-config": "workspace:^" "@types/semver": "npm:^7.5.3" eslint: "npm:^9.9.1" @@ -2108,7 +2094,7 @@ __metadata: languageName: unknown linkType: soft -"@solarwinds-apm/sdk@workspace:^, @solarwinds-apm/sdk@workspace:packages/sdk": +"@solarwinds-apm/sdk@workspace:packages/sdk": version: 0.0.0-use.local resolution: "@solarwinds-apm/sdk@workspace:packages/sdk" dependencies: @@ -3195,13 +3181,6 @@ __metadata: languageName: node linkType: hard -"bignumber.js@npm:^9.0.0": - version: 9.1.2 - resolution: "bignumber.js@npm:9.1.2" - checksum: 10c0/e17786545433f3110b868725c449fa9625366a6e675cd70eb39b60938d6adbd0158cb4b3ad4f306ce817165d37e63f4aa3098ba4110db1d9a3b9f66abfbaf10d - languageName: node - linkType: hard - "binary-extensions@npm:^2.0.0": version: 2.2.0 resolution: "binary-extensions@npm:2.2.0" @@ -4625,13 +4604,6 @@ __metadata: languageName: node linkType: hard -"extend@npm:^3.0.2": - version: 3.0.2 - resolution: "extend@npm:3.0.2" - checksum: 10c0/73bf6e27406e80aa3e85b0d1c4fd987261e628064e170ca781125c0b635a3dabad5e05adbf07595ea0cf1e6c5396cacb214af933da7cbaf24fe75ff14818e8f9 - languageName: node - linkType: hard - "fast-content-type-parse@npm:^1.1.0": version: 1.1.0 resolution: "fast-content-type-parse@npm:1.1.0" @@ -4989,29 +4961,6 @@ __metadata: languageName: node linkType: hard -"gaxios@npm:^6.0.0": - version: 6.7.1 - resolution: "gaxios@npm:6.7.1" - dependencies: - extend: "npm:^3.0.2" - https-proxy-agent: "npm:^7.0.1" - is-stream: "npm:^2.0.0" - node-fetch: "npm:^2.6.9" - uuid: "npm:^9.0.1" - checksum: 10c0/53e92088470661c5bc493a1de29d05aff58b1f0009ec5e7903f730f892c3642a93e264e61904383741ccbab1ce6e519f12a985bba91e13527678b32ee6d7d3fd - languageName: node - linkType: hard - -"gcp-metadata@npm:^6.0.0": - version: 6.1.0 - resolution: "gcp-metadata@npm:6.1.0" - dependencies: - gaxios: "npm:^6.0.0" - json-bigint: "npm:^1.0.0" - checksum: 10c0/0f84f8c0b974e79d0da0f3063023486e53d7982ce86c4b5871e4ee3b1fc4e7f76fcc05f6342aa0ded5023f1a499c21ab97743a498b31f3aa299905226d1f66ab - languageName: node - linkType: hard - "generate-function@npm:^2.3.1": version: 2.3.1 resolution: "generate-function@npm:2.3.1" @@ -5878,15 +5827,6 @@ __metadata: languageName: node linkType: hard -"json-bigint@npm:^1.0.0": - version: 1.0.0 - resolution: "json-bigint@npm:1.0.0" - dependencies: - bignumber.js: "npm:^9.0.0" - checksum: 10c0/e3f34e43be3284b573ea150a3890c92f06d54d8ded72894556357946aeed9877fd795f62f37fe16509af189fd314ab1104d0fd0f163746ad231b9f378f5b33f4 - languageName: node - linkType: hard - "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -6610,20 +6550,6 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.9": - version: 2.7.0 - resolution: "node-fetch@npm:2.7.0" - dependencies: - whatwg-url: "npm:^5.0.0" - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8 - languageName: node - linkType: hard - "node-gyp@npm:latest": version: 10.0.1 resolution: "node-gyp@npm:10.0.1" @@ -6644,6 +6570,13 @@ __metadata: languageName: node linkType: hard +"node-releases@npm:^2.0.18": + version: 2.0.18 + resolution: "node-releases@npm:2.0.18" + checksum: 10c0/786ac9db9d7226339e1dc84bbb42007cb054a346bd9257e6aa154d294f01bc6a6cddb1348fa099f079be6580acbb470e3c048effd5f719325abd0179e566fd27 + languageName: node + linkType: hard + "nopt@npm:^7.0.0": version: 7.2.0 resolution: "nopt@npm:7.2.0" @@ -8085,20 +8018,18 @@ __metadata: "@solarwinds-apm/eslint-config": "workspace:^" "@solarwinds-apm/histogram": "workspace:^" "@solarwinds-apm/instrumentations": "workspace:^" - "@solarwinds-apm/lazy": "workspace:^" "@solarwinds-apm/module": "workspace:^" "@solarwinds-apm/proto": "workspace:^" "@solarwinds-apm/rollup-config": "workspace:^" "@solarwinds-apm/sampling": "workspace:^" - "@solarwinds-apm/sdk": "workspace:^" "@solarwinds-apm/test": "workspace:^" "@types/node": "npm:^18.19.43" "@types/semver": "npm:^7.5.3" eslint: "npm:^9.9.1" json-stringify-safe: "npm:^5.0.1" + node-releases: "npm:^2.0.18" prettier: "npm:^3.3.3" rollup: "npm:^4.3.0" - semver: "npm:^7.5.4" typescript: "npm:~5.5.3" zod: "npm:^3.22.4" peerDependencies: @@ -8463,13 +8394,6 @@ __metadata: languageName: node linkType: hard -"tr46@npm:~0.0.3": - version: 0.0.3 - resolution: "tr46@npm:0.0.3" - checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11 - languageName: node - linkType: hard - "tree-kill@npm:^1.2.2": version: 1.2.2 resolution: "tree-kill@npm:1.2.2" @@ -8838,15 +8762,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.1": - version: 9.0.1 - resolution: "uuid@npm:9.0.1" - bin: - uuid: dist/bin/uuid - checksum: 10c0/1607dd32ac7fc22f2d8f77051e6a64845c9bce5cd3dd8aa0070c074ec73e666a1f63c7b4e0f4bf2bc8b9d59dc85a15e17807446d9d2b17c8485fbc2147b27f9b - languageName: node - linkType: hard - "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1" @@ -8881,23 +8796,6 @@ __metadata: languageName: node linkType: hard -"webidl-conversions@npm:^3.0.0": - version: 3.0.1 - resolution: "webidl-conversions@npm:3.0.1" - checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db - languageName: node - linkType: hard - -"whatwg-url@npm:^5.0.0": - version: 5.0.0 - resolution: "whatwg-url@npm:5.0.0" - dependencies: - tr46: "npm:~0.0.3" - webidl-conversions: "npm:^3.0.0" - checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5 - languageName: node - linkType: hard - "which-boxed-primitive@npm:^1.0.2": version: 1.0.2 resolution: "which-boxed-primitive@npm:1.0.2" From 7b21dce9b30290bec3ba51deb1ad3fbb6a7a09ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Fri, 11 Oct 2024 21:44:50 -0400 Subject: [PATCH 2/9] add back missing hooks registration --- packages/solarwinds-apm/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/solarwinds-apm/src/index.ts b/packages/solarwinds-apm/src/index.ts index 13729c2a..84ca47ba 100644 --- a/packages/solarwinds-apm/src/index.ts +++ b/packages/solarwinds-apm/src/index.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { IS_SERVERLESS } from "@solarwinds-apm/module" +import { register } from "module" import { init } from "./init.js" import { global } from "./storage.js" @@ -26,6 +27,7 @@ if (!IS_SERVERLESS) { const first = Symbol() if (global("init", () => first) === first) { try { + register("./hooks.js", import.meta.url) await init() } catch (error) { console.error(error) From 4e5b709fc7f07e82570ff1c00e6122f74491702f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Fri, 11 Oct 2024 21:57:32 -0400 Subject: [PATCH 3/9] directly import response time processor --- packages/solarwinds-apm/src/init.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/solarwinds-apm/src/init.ts b/packages/solarwinds-apm/src/init.ts index fda165ca..703aad8b 100644 --- a/packages/solarwinds-apm/src/init.ts +++ b/packages/solarwinds-apm/src/init.ts @@ -48,6 +48,7 @@ import { type Configuration, printError, read } from "./config.js" import { componentLogger, Logger } from "./logger.js" import { patch } from "./patches.js" import { ParentSpanProcessor } from "./processing/parent-span.js" +import { ResponseTimeProcessor } from "./processing/response-time.js" import { TransactionNameProcessor } from "./processing/transaction-name.js" import { RequestHeadersPropagator, @@ -204,12 +205,10 @@ async function initTracing( Promise.resolve((sampler as AppopticsSampler).isReady(timeout)), }) } else { - const [{ GrpcSampler }, { TraceExporter }, { ResponseTimeProcessor }] = - await Promise.all([ - import("./sampling/grpc.js"), - import("./exporters/traces.js"), - import("./processing/response-time.js"), - ]) + const [{ GrpcSampler }, { TraceExporter }] = await Promise.all([ + import("./sampling/grpc.js"), + import("./exporters/traces.js"), + ]) sampler = new GrpcSampler(config) processors = [ From e96a946ffea355a4a9cd560319153992cd2dff77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Fri, 11 Oct 2024 22:13:53 -0400 Subject: [PATCH 4/9] improve commonjs handling --- packages/rollup-config/index.js | 2 +- packages/solarwinds-apm/src/commonjs/api.js | 2 ++ packages/solarwinds-apm/src/commonjs/index.js | 29 +++++++++---------- .../solarwinds-apm/src/commonjs/version.js | 2 +- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/rollup-config/index.js b/packages/rollup-config/index.js index 35d555b9..11c0e486 100644 --- a/packages/rollup-config/index.js +++ b/packages/rollup-config/index.js @@ -23,7 +23,7 @@ import typescript from "@rollup/plugin-typescript" import globby from "globby" import nodeExternals from "rollup-plugin-node-externals" -const FORMATS = ["es"] +const FORMATS = ["es", "cjs"] async function task(src, dist, format, sources) { const dir = path.join(dist, format) diff --git a/packages/solarwinds-apm/src/commonjs/api.js b/packages/solarwinds-apm/src/commonjs/api.js index f57e2725..96166a87 100644 --- a/packages/solarwinds-apm/src/commonjs/api.js +++ b/packages/solarwinds-apm/src/commonjs/api.js @@ -1,3 +1,5 @@ +"use strict"; + /* Copyright 2023-2024 SolarWinds Worldwide, LLC. diff --git a/packages/solarwinds-apm/src/commonjs/index.js b/packages/solarwinds-apm/src/commonjs/index.js index 959847ac..1fc8f4f4 100644 --- a/packages/solarwinds-apm/src/commonjs/index.js +++ b/packages/solarwinds-apm/src/commonjs/index.js @@ -1,19 +1,16 @@ -/* -Copyright 2023-2024 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. -*/ +/* eslint-disable */ +"use strict"; if (require("./version")) { - module.exports = require("./api"); + var init = Symbol.for("solarwinds-apm / init"); + if (init in global) { + module.exports = require("./api"); + } else { + // this will not trigger if customers use the --import flag then use require, + // it will only trigger if they only ever use require + console.warn( + "This library (solarwinds-apm) no longer supports loading via require. " + + "The application may not be instrumented." + ); + } } diff --git a/packages/solarwinds-apm/src/commonjs/version.js b/packages/solarwinds-apm/src/commonjs/version.js index 72df9e62..ed36d130 100644 --- a/packages/solarwinds-apm/src/commonjs/version.js +++ b/packages/solarwinds-apm/src/commonjs/version.js @@ -32,7 +32,7 @@ try { process.version + ") has reached End Of Life over one year ago (" + versions[major].end + - "). It is no longer supported by this library (solarwinds-apm) and things may break unexpectedly. " + + "). It is no longer supported by this library (solarwinds-apm) and the application will not be instrumented. " + "SolarWinds STRONGLY recommends customers use a non-EOL Node.js version receiving security updates."; throw message; From c89f0785c758b8ef1b9fe0a62f59bcd3a7ae4790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Fri, 11 Oct 2024 22:19:33 -0400 Subject: [PATCH 5/9] improve api handling --- packages/solarwinds-apm/src/api.ts | 3 +-- packages/solarwinds-apm/src/init.ts | 39 +++++++++++------------------ 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/packages/solarwinds-apm/src/api.ts b/packages/solarwinds-apm/src/api.ts index 7753db04..5edac5a0 100644 --- a/packages/solarwinds-apm/src/api.ts +++ b/packages/solarwinds-apm/src/api.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { API } from "./init.js" +import { api } from "./init.js" /** * Wait until the library is ready to sample traces @@ -25,7 +25,6 @@ import { API } from "./init.js" * @returns Whether the library is ready */ export async function waitUntilReady(timeout: number): Promise { - const api = await API.value return api.waitUntilReady(timeout) } diff --git a/packages/solarwinds-apm/src/init.ts b/packages/solarwinds-apm/src/init.ts index 703aad8b..b07338ca 100644 --- a/packages/solarwinds-apm/src/init.ts +++ b/packages/solarwinds-apm/src/init.ts @@ -56,20 +56,15 @@ import { } from "./propagation/headers.js" import { TraceContextPropagator } from "./propagation/trace-context.js" import { type GrpcSampler } from "./sampling/grpc.js" -import { global } from "./storage.js" import { VERSION } from "./version.js" +// portion of the public API that depends on initialisation interface Api { waitUntilReady: (timeout: number) => Promise } -export const API = global("api", () => { - const api: { value: Promise; resolve: (value: Api) => void } = { - value: null!, - resolve: null!, - } - api.value = new Promise((resolve) => (api.resolve = resolve)) - return api -}) +export const api: Api = { + waitUntilReady: () => Promise.resolve(false), +} export async function init() { let config: Configuration @@ -200,10 +195,8 @@ async function initTracing( new ParentSpanProcessor(), ] - API.resolve({ - waitUntilReady: (timeout) => - Promise.resolve((sampler as AppopticsSampler).isReady(timeout)), - }) + api.waitUntilReady = (timeout) => + Promise.resolve((sampler as AppopticsSampler).isReady(timeout)) } else { const [{ GrpcSampler }, { TraceExporter }] = await Promise.all([ import("./sampling/grpc.js"), @@ -218,17 +211,15 @@ async function initTracing( new ParentSpanProcessor(), ] - API.resolve({ - waitUntilReady: (timeout) => - new Promise((resolve) => { - void (sampler as GrpcSampler).ready.then(() => { - resolve(true) - }) - void setTimeout(timeout).then(() => { - resolve(false) - }) - }), - }) + api.waitUntilReady = (timeout) => + new Promise((resolve) => { + void (sampler as GrpcSampler).ready.then(() => { + resolve(true) + }) + void setTimeout(timeout).then(() => { + resolve(false) + }) + }) } const provider = new NodeTracerProvider({ From 34249ae50ecdbddf234d2b2ff9cd2b908685b9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Fri, 11 Oct 2024 22:28:03 -0400 Subject: [PATCH 6/9] improve commonjs handling more --- packages/solarwinds-apm/src/commonjs/api.js | 2 -- packages/solarwinds-apm/src/commonjs/index.js | 7 +++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/solarwinds-apm/src/commonjs/api.js b/packages/solarwinds-apm/src/commonjs/api.js index 96166a87..e6808cf2 100644 --- a/packages/solarwinds-apm/src/commonjs/api.js +++ b/packages/solarwinds-apm/src/commonjs/api.js @@ -16,8 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -module.exports.VERSION = "0.0.0-0"; - module.exports.setTransactionName = function setTransactionName(name) { if (api) { return api.setTransactionName(name); diff --git a/packages/solarwinds-apm/src/commonjs/index.js b/packages/solarwinds-apm/src/commonjs/index.js index 1fc8f4f4..be61f028 100644 --- a/packages/solarwinds-apm/src/commonjs/index.js +++ b/packages/solarwinds-apm/src/commonjs/index.js @@ -2,10 +2,7 @@ "use strict"; if (require("./version")) { - var init = Symbol.for("solarwinds-apm / init"); - if (init in global) { - module.exports = require("./api"); - } else { + if ((!Symbol.for("solarwinds-apm / init")) in global) { // this will not trigger if customers use the --import flag then use require, // it will only trigger if they only ever use require console.warn( @@ -13,4 +10,6 @@ if (require("./version")) { "The application may not be instrumented." ); } + + module.exports = require("./api"); } From 420d5c2ec6d28d820fd59787169489681f5fd8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Thu, 17 Oct 2024 14:39:09 -0400 Subject: [PATCH 7/9] don't exit process --- packages/solarwinds-apm/src/commonjs/version.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/solarwinds-apm/src/commonjs/version.js b/packages/solarwinds-apm/src/commonjs/version.js index ed36d130..3bf9cb0c 100644 --- a/packages/solarwinds-apm/src/commonjs/version.js +++ b/packages/solarwinds-apm/src/commonjs/version.js @@ -10,8 +10,7 @@ try { typeof process.version !== "string" || process.version.substring(0, 2) === "v0" ) { - console.error("UPDATE YOUR NODE.JS VERSION IMMEDIATELY."); - process.exit(1); + throw "UPDATE YOUR NODE.JS VERSION IMMEDIATELY."; } var file = require.resolve( From 4298e8e827f02df242bf7f6393c29926f0111194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Thu, 17 Oct 2024 14:43:44 -0400 Subject: [PATCH 8/9] use componentLogger helper everywhere --- packages/solarwinds-apm/src/appoptics/reporter.ts | 6 ++---- packages/solarwinds-apm/src/appoptics/sampler.ts | 6 ++++++ packages/solarwinds-apm/src/init.ts | 5 +---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/solarwinds-apm/src/appoptics/reporter.ts b/packages/solarwinds-apm/src/appoptics/reporter.ts index 02b62632..ce5af5f8 100644 --- a/packages/solarwinds-apm/src/appoptics/reporter.ts +++ b/packages/solarwinds-apm/src/appoptics/reporter.ts @@ -16,7 +16,6 @@ limitations under the License. import { type Attributes, - diag, type DiagLogFunction, type DiagLogger, DiagLogLevel, @@ -30,6 +29,7 @@ import { import { oboe } from "@solarwinds-apm/bindings" import { type Configuration } from "../config.js" +import { componentLogger } from "../logger.js" import { modules } from "../metadata.js" import { VERSION } from "../version.js" import certificate from "./certificate.js" @@ -67,9 +67,7 @@ export async function reporter( token_bucket_rate: oboe.SETTINGS_UNSET, }) - const logger = diag.createComponentLogger({ - namespace: `[solarwinds-apm / oboe]`, - }) + const logger = componentLogger({ name: "oboe" }) oboe.debug_log_add((level, sourceName, sourceLine, message) => { const log = oboeLevelToOtelLogger(level, logger) diff --git a/packages/solarwinds-apm/src/appoptics/sampler.ts b/packages/solarwinds-apm/src/appoptics/sampler.ts index b2394d31..4eb9a98c 100644 --- a/packages/solarwinds-apm/src/appoptics/sampler.ts +++ b/packages/solarwinds-apm/src/appoptics/sampler.ts @@ -46,6 +46,8 @@ import { type TriggerTrace, } from "@solarwinds-apm/sampling" +import { type Configuration } from "../config.js" +import { componentLogger } from "../logger.js" import { HEADERS_STORAGE } from "../propagation/headers.js" import { swValue } from "../propagation/trace-context.js" import { Sampler } from "../sampling/sampler.js" @@ -55,6 +57,10 @@ export function traceParent(spanContext: SpanContext): string { } export class AppopticsSampler extends Sampler { + constructor(config: Configuration) { + super(config, componentLogger(AppopticsSampler)) + } + override shouldSample(...params: SampleParams): SamplingResult { const [context, , , , attributes] = params diff --git a/packages/solarwinds-apm/src/init.ts b/packages/solarwinds-apm/src/init.ts index b07338ca..504e1978 100644 --- a/packages/solarwinds-apm/src/init.ts +++ b/packages/solarwinds-apm/src/init.ts @@ -185,10 +185,7 @@ async function initTracing( import("./appoptics/processing/inbound-metrics.js"), ]) - sampler = new AppopticsSampler( - config, - diag.createComponentLogger({ namespace: "[solarwinds-apm / sampler]" }), - ) + sampler = new AppopticsSampler(config) processors = [ new AppopticsInboundMetricsProcessor(config), new BatchSpanProcessor(new AppopticsTraceExporter(oboe)), From 592eefb47ca5f1edce7bd1ffed5efa8fb0888bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Thu, 17 Oct 2024 14:47:00 -0400 Subject: [PATCH 9/9] wait for async resource attributes --- packages/solarwinds-apm/src/init.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/solarwinds-apm/src/init.ts b/packages/solarwinds-apm/src/init.ts index 504e1978..76001a66 100644 --- a/packages/solarwinds-apm/src/init.ts +++ b/packages/solarwinds-apm/src/init.ts @@ -109,6 +109,10 @@ export async function init() { }), ) + if (resource.asyncAttributesPending) { + await resource.waitForAsyncAttributes?.() + } + let oboe: oboe.Reporter | undefined if (config.legacy) { logger.debug("using oboe")