From f8164b94b3861dc72ba4b44f81905a04c7eec498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Wed, 15 Nov 2023 16:40:32 -0800 Subject: [PATCH 1/6] initial OboeAPI integration --- package.json | 2 +- packages/bindings/index.js | 6 +- packages/bindings/package.json | 19 +-- packages/module/README.md | 2 +- packages/module/src/index.ts | 6 + packages/sdk/package.json | 10 +- packages/sdk/src/metrics/serverless.ts | 44 ++++++ packages/sdk/src/otlp-exporter.ts | 41 ++++++ packages/sdk/src/patches/lambda.ts | 5 +- packages/sdk/src/sampler.ts | 26 +++- packages/solarwinds-apm/package.json | 2 + packages/solarwinds-apm/src/config.ts | 39 +++--- packages/solarwinds-apm/src/init.ts | 184 +++++++++++++++---------- tsconfig.base.json | 1 + yarn.lock | 7 + 15 files changed, 285 insertions(+), 109 deletions(-) create mode 100644 packages/sdk/src/metrics/serverless.ts create mode 100644 packages/sdk/src/otlp-exporter.ts diff --git a/package.json b/package.json index e13ee7f6..dd22f700 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@solarwinds-apm/solarwinds-apm", "private": true, "license": "Apache-2.0", - "packageManager": "yarn@4.0.1", + "packageManager": "yarn@4.0.2", "workspaces": [ "examples", "examples/*", diff --git a/packages/bindings/index.js b/packages/bindings/index.js index 5915e96f..42979ebf 100644 --- a/packages/bindings/index.js +++ b/packages/bindings/index.js @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +const { IS_SERVERLESS } = require("@solarwinds-apm/module") + function triple() { const platform = process.platform const arch = process.arch @@ -29,9 +31,7 @@ function triple() { } function serverless() { - const isServerless = "AWS_LAMBDA_FUNCTION_NAME" in process.env - - if (isServerless) { + if (IS_SERVERLESS) { return "-serverless" } else { return "" diff --git a/packages/bindings/package.json b/packages/bindings/package.json index 7b7143f6..c49486d3 100644 --- a/packages/bindings/package.json +++ b/packages/bindings/package.json @@ -37,6 +37,17 @@ "test": "swtest", "oboe": "node oboe.js" }, + "dependencies": { + "@solarwinds-apm/module": "workspace:^" + }, + "optionalDependencies": { + "@solarwinds-apm/bindings-linux-arm64-gnu": "workspace:*", + "@solarwinds-apm/bindings-linux-arm64-gnu-serverless": "workspace:*", + "@solarwinds-apm/bindings-linux-arm64-musl": "workspace:*", + "@solarwinds-apm/bindings-linux-x64-gnu": "workspace:*", + "@solarwinds-apm/bindings-linux-x64-gnu-serverless": "workspace:*", + "@solarwinds-apm/bindings-linux-x64-musl": "workspace:*" + }, "devDependencies": { "@solarwinds-apm/eslint-config": "workspace:^", "@solarwinds-apm/test": "workspace:^", @@ -47,14 +58,6 @@ "typescript": "^5.2.2", "zig-build": "^0.3.0" }, - "optionalDependencies": { - "@solarwinds-apm/bindings-linux-arm64-gnu": "workspace:*", - "@solarwinds-apm/bindings-linux-arm64-gnu-serverless": "workspace:*", - "@solarwinds-apm/bindings-linux-arm64-musl": "workspace:*", - "@solarwinds-apm/bindings-linux-x64-gnu": "workspace:*", - "@solarwinds-apm/bindings-linux-x64-gnu-serverless": "workspace:*", - "@solarwinds-apm/bindings-linux-x64-musl": "workspace:*" - }, "engines": { "node": ">=16.13.0" }, diff --git a/packages/module/README.md b/packages/module/README.md index 13e305fa..5c2582a4 100644 --- a/packages/module/README.md +++ b/packages/module/README.md @@ -1,3 +1,3 @@ # @solarwinds-apm/module -Utilities for dealing with differences between CommonJS and ES Modules. +Utilities for obtaining information about the current module. diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 99287a45..ccc56db7 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { env } from "node:process" + /** * Finds the current call site. Useful to replace `import.meta.url` or `__dirname`, * or to get more than just a file name. @@ -35,3 +37,7 @@ export function callsite(): NodeJS.CallSite { Error.prepareStackTrace = prepareStackTrace } } + +export const IS_AWS_LAMBDA = "AWS_LAMBDA_FUNCTION_NAME" in env + +export const IS_SERVERLESS = IS_AWS_LAMBDA diff --git a/packages/sdk/package.json b/packages/sdk/package.json index fccfae07..9a38abba 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -15,7 +15,8 @@ }, "type": "commonjs", "exports": { - ".": "./dist/index.js" + ".": "./dist/index.js", + "./otlp-exporter": "./dist/otlp-exporter.js" }, "main": "./dist/index.js", "files": [ @@ -41,14 +42,19 @@ "@solarwinds-apm/dependencies": "workspace:^", "@solarwinds-apm/histogram": "workspace:^", "@solarwinds-apm/lazy": "workspace:^", + "@solarwinds-apm/module": "workspace:^", "semver": "^7.5.4" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": "^1.3.0", + "@opentelemetry/exporter-trace-otlp-grpc": ">=0.34.0" }, "peerDependenciesMeta": { "@opentelemetry/api": { "optional": false + }, + "@opentelemetry/exporter-trace-otlp-grpc": { + "optional": true } }, "devDependencies": { diff --git a/packages/sdk/src/metrics/serverless.ts b/packages/sdk/src/metrics/serverless.ts new file mode 100644 index 00000000..465dd712 --- /dev/null +++ b/packages/sdk/src/metrics/serverless.ts @@ -0,0 +1,44 @@ +/* +Copyright 2023 SolarWinds Worldwide, LLC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { metrics } from "@opentelemetry/api" +import { type oboe } from "@solarwinds-apm/bindings" +import { lazy } from "@solarwinds-apm/lazy" + +const meter = metrics.getMeter("sw.apm.sampling.metrics") + +const counters = { + RequestCount: lazy(() => meter.createCounter("trace.service.request_count")), + TokenBucketExhaustionCount: lazy(() => + meter.createCounter("trace.service.tokenbucket_exhaustion_count"), + ), + TraceCount: lazy(() => meter.createCounter("trace.service.tracecount")), + SampleCount: lazy(() => meter.createCounter("trace.service.samplecount")), + ThroughTraceCount: lazy(() => + meter.createCounter("trace.service.through_trace_count"), + ), + TriggeredTraceCount: lazy(() => + meter.createCounter("trace.service.triggered_trace_count"), + ), +} + +export function recordServerlessMetrics(serverlessApi: oboe.OboeAPI) { + for (const [name, counter] of Object.entries(counters)) { + const method = `consume${name}` as const + const value = serverlessApi[method]() + if (value !== false) counter.add(value) + } +} diff --git a/packages/sdk/src/otlp-exporter.ts b/packages/sdk/src/otlp-exporter.ts new file mode 100644 index 00000000..912c45c7 --- /dev/null +++ b/packages/sdk/src/otlp-exporter.ts @@ -0,0 +1,41 @@ +/* +Copyright 2023 SolarWinds Worldwide, LLC. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { type ExportResult } from "@opentelemetry/core" +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc" +import { type ReadableSpan } from "@opentelemetry/sdk-trace-base" + +import { cache } from "./cache" + +export class SwOtlpExporter extends OTLPTraceExporter { + override export( + spans: ReadableSpan[], + resultCallback: (result: ExportResult) => void, + ): void { + for (const span of spans) { + const context = span.spanContext() + + const txname = cache.get(context)?.txname + if (txname) { + span.attributes["sw.transaction"] = txname + } + + cache.clear(context) + } + + super.export(spans, resultCallback) + } +} diff --git a/packages/sdk/src/patches/lambda.ts b/packages/sdk/src/patches/lambda.ts index 02ddbc9c..9c02fb79 100644 --- a/packages/sdk/src/patches/lambda.ts +++ b/packages/sdk/src/patches/lambda.ts @@ -14,12 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { env } from "node:process" - import { type InstrumentationConfig } from "@opentelemetry/instrumentation" +import { IS_AWS_LAMBDA } from "@solarwinds-apm/module" import { type Patch } from "." export const patch: Patch = (config) => ({ - enabled: config.enabled ?? "AWS_LAMBDA_FUNCTION_NAME" in env, + enabled: config.enabled ?? IS_AWS_LAMBDA, }) diff --git a/packages/sdk/src/sampler.ts b/packages/sdk/src/sampler.ts index ddccd7be..eeb09272 100644 --- a/packages/sdk/src/sampler.ts +++ b/packages/sdk/src/sampler.ts @@ -46,6 +46,7 @@ import { TRACESTATE_TRACE_OPTIONS_RESPONSE_KEY, } from "./context" import { OboeError } from "./error" +import { recordServerlessMetrics } from "./metrics/serverless" const ATTRIBUTES_SW_KEYS_KEY = "SWKeys" const ATTRIBUTES_TRACESTATE_CAPTURE_KEY = "sw.w3c.tracestate" @@ -57,10 +58,29 @@ const TRACE_OPTIONS_RESPONSE_TRIGGER_IGNORED = "ignored" const TRACE_OPTIONS_RESPONSE_TRIGGER_NOT_REQUESTED = "not-requested" export class SwSampler implements Sampler { + private readonly oboeDecisionFunction: ( + options: oboe.DecisionOptions, + ) => oboe.DecisionResult + private readonly recordMetricsFunction: () => void + constructor( private readonly config: SwConfiguration, private readonly logger: DiagLogger, - ) {} + serverlessApi: oboe.OboeAPI | undefined, + ) { + if (serverlessApi) { + this.oboeDecisionFunction = + serverlessApi.getTracingDecision.bind(serverlessApi) + this.recordMetricsFunction = () => { + recordServerlessMetrics(serverlessApi) + } + } else { + this.oboeDecisionFunction = oboe.Context.getDecisions + this.recordMetricsFunction = () => { + // noop + } + } + } shouldSample( parentContext: Context, @@ -116,6 +136,8 @@ export class SwSampler implements Sampler { traceOptions, traceState, ) + + this.recordMetricsFunction() return { decision, traceState, attributes: newAttributes } } @@ -140,7 +162,7 @@ export class SwSampler implements Sampler { traceparent = traceParent(parentSpanContext) } - return oboe.Context.getDecisions({ + return this.oboeDecisionFunction({ in_xtrace: traceparent, custom_sample_rate: oboe.SETTINGS_UNSET, custom_tracing_mode: tracingMode, diff --git a/packages/solarwinds-apm/package.json b/packages/solarwinds-apm/package.json index e2530305..a7adb89f 100644 --- a/packages/solarwinds-apm/package.json +++ b/packages/solarwinds-apm/package.json @@ -60,10 +60,12 @@ "@opentelemetry/instrumentation": "~0.45.1", "@opentelemetry/resources": "~1.18.1", "@opentelemetry/sdk-metrics": "~1.18.1", + "@opentelemetry/sdk-trace-base": "~1.18.1", "@opentelemetry/sdk-trace-node": "~1.18.1", "@opentelemetry/semantic-conventions": "~1.18.1", "@solarwinds-apm/bindings": "workspace:^", "@solarwinds-apm/instrumentations": "workspace:^", + "@solarwinds-apm/lazy": "workspace:^", "@solarwinds-apm/module": "workspace:^", "@solarwinds-apm/sdk": "workspace:^", "semver": "^7.5.4", diff --git a/packages/solarwinds-apm/src/config.ts b/packages/solarwinds-apm/src/config.ts index 0e08adf2..1468d0bb 100644 --- a/packages/solarwinds-apm/src/config.ts +++ b/packages/solarwinds-apm/src/config.ts @@ -25,7 +25,7 @@ import { InstrumentationBase } from "@opentelemetry/instrumentation" import { View } from "@opentelemetry/sdk-metrics" import { oboe } from "@solarwinds-apm/bindings" import { type InstrumentationConfigMap } from "@solarwinds-apm/instrumentations" -import { callsite } from "@solarwinds-apm/module" +import { callsite, IS_SERVERLESS } from "@solarwinds-apm/module" import { type SwConfiguration } from "@solarwinds-apm/sdk" import { z } from "zod" @@ -33,6 +33,8 @@ import aoCert from "./appoptics.crt.js" const r = createRequire(callsite().getFileName()!) +const otelEnv = getEnvWithoutDefaults() + const boolean = z.union([ z.boolean(), z @@ -62,7 +64,7 @@ const serviceKey = z const [token, ...name] = k.split(":") return { token: token!, - name: getEnvWithoutDefaults().OTEL_SERVICE_NAME ?? name.join(":"), + name: otelEnv.OTEL_SERVICE_NAME ?? name.join(":"), } }) @@ -83,9 +85,11 @@ const tracingMode = z .transform((m) => m === "enabled") const logLevel = z - .enum(["verbose", "debug", "info", "warn", "error", "none"]) + .enum(["all", "verbose", "debug", "info", "warn", "error", "none"]) .transform((l) => { switch (l) { + case "all": + return DiagLogLevel.ALL case "verbose": return DiagLogLevel.VERBOSE case "debug": @@ -138,7 +142,7 @@ const schema = z.object({ proxy: z.string().optional(), logLevel: logLevel.default("info"), triggerTraceEnabled: boolean.default(true), - runtimeMetrics: boolean.default(true), + runtimeMetrics: boolean.default(!IS_SERVERLESS), tracingMode: tracingMode.optional(), insertTraceContextIntoLogs: boolean.default(false), insertTraceContextIntoQueries: boolean.default(false), @@ -157,12 +161,14 @@ const schema = z.object({ }) .default({}), - experimental: z + dev: z .object({ - otlpTraces: boolean.default(false), - otlpMetrics: boolean.default(false), - swTraces: boolean.default(true), - swMetrics: boolean.default(true), + otlpTraces: boolean.default(IS_SERVERLESS), + otlpMetrics: boolean.default(IS_SERVERLESS), + swTraces: boolean.default(!IS_SERVERLESS), + swMetrics: boolean.default(!IS_SERVERLESS), + serverlessSampling: boolean.default(IS_SERVERLESS), + initMessage: boolean.default(!IS_SERVERLESS), }) .default({}), }) @@ -176,29 +182,26 @@ export interface ExtendedSwConfiguration extends SwConfiguration { instrumentations: Instrumentations metrics: Metrics - experimental: z.infer["experimental"] + dev: z.infer["dev"] } const ENV_PREFIX = "SW_APM_" -const ENV_PREFIX_EXPERIMENTAL = `${ENV_PREFIX}EXPERIMENTAL_` +const ENV_PREFIX_DEV = `${ENV_PREFIX}DEV_` const DEFAULT_FILE_NAME = "solarwinds.apm.config" export function readConfig(): ExtendedSwConfiguration { const env = envObject() - const experimentalEnv = envObject(ENV_PREFIX_EXPERIMENTAL) + const devEnv = envObject(ENV_PREFIX_DEV) const path = filePath() const file = path ? readConfigFile(path) : {} - const experimentalFile = - file.experimental && typeof file.experimental === "object" - ? file.experimental - : {} + const devFile = file.dev && typeof file.dev === "object" ? file.dev : {} const raw = schema.parse({ ...file, ...env, - experimental: { ...experimentalFile, ...experimentalEnv }, + experimental: { ...devFile, ...devEnv }, }) const config: ExtendedSwConfiguration = { @@ -207,7 +210,7 @@ export function readConfig(): ExtendedSwConfiguration { serviceName: raw.serviceKey.name, certificate: raw.trustedpath, oboeLogLevel: otelLevelToOboeLevel(raw.logLevel), - otelLogLevel: raw.logLevel, + otelLogLevel: otelEnv.OTEL_LOG_LEVEL ?? raw.logLevel, } if (config.collector?.includes("appoptics.com")) { diff --git a/packages/solarwinds-apm/src/init.ts b/packages/solarwinds-apm/src/init.ts index a258bd6f..dc0fc95e 100644 --- a/packages/solarwinds-apm/src/init.ts +++ b/packages/solarwinds-apm/src/init.ts @@ -19,6 +19,7 @@ import { type DiagLogFunction, type DiagLogger, metrics, + type TextMapPropagator, type TracerProvider, } from "@opentelemetry/api" import { CompositePropagator, W3CBaggagePropagator } from "@opentelemetry/core" @@ -26,10 +27,11 @@ import { registerInstrumentations } from "@opentelemetry/instrumentation" import { Resource } from "@opentelemetry/resources" import { MeterProvider, + type MetricReader, PeriodicExportingMetricReader, } from "@opentelemetry/sdk-metrics" +import { type Sampler, type SpanProcessor } from "@opentelemetry/sdk-trace-base" import { - BatchSpanProcessor, NodeTracerProvider, ParentBasedSampler, } from "@opentelemetry/sdk-trace-node" @@ -39,6 +41,8 @@ import { getDetectedResource, getInstrumentations, } from "@solarwinds-apm/instrumentations" +import { lazy } from "@solarwinds-apm/lazy" +import { IS_SERVERLESS } from "@solarwinds-apm/module" import * as sdk from "@solarwinds-apm/sdk" import { version } from "../package.json" @@ -81,7 +85,7 @@ export async function init() { return } - // initialize instrumentations before any asynchronous code + // initialize instrumentations before any asynchronous code or imports const registerInstrumentations = initInstrumentations(config) const resource = Resource.default() @@ -91,7 +95,7 @@ export async function init() { [SemanticResourceAttributes.SERVICE_NAME]: config.serviceName, }), ) - const reporter = sdk.createReporter(config) + const reporter = lazy(() => sdk.createReporter(config)) oboe.debug_log_add((module, level, sourceName, sourceLine, message) => { const logger = diag.createComponentLogger({ @@ -110,7 +114,7 @@ export async function init() { const [tracerProvider, meterProvider] = await Promise.all([ initTracing(config, resource, reporter), initMetrics(config, resource, reporter, logger), - initMessage(resource, reporter), + initMessage(config, resource, reporter), ]) registerInstrumentations(tracerProvider, meterProvider) } @@ -142,55 +146,17 @@ async function initTracing( resource: Resource, reporter: oboe.Reporter, ) { - const sampler = new sdk.SwSampler( - config, - diag.createComponentLogger({ namespace: "[sw/sampler]" }), - ) - const exporter = new sdk.SwExporter( - reporter, - diag.createComponentLogger({ namespace: "[sw/exporter]" }), - ) - - const parentInfoProcessor = new sdk.SwParentInfoSpanProcessor() - const inboundMetricsProcessor = new sdk.SwInboundMetricsSpanProcessor() - const spanProcessor = new sdk.CompoundSpanProcessor(exporter, [ - parentInfoProcessor, - inboundMetricsProcessor, - ]) - - const baggagePropagator = new W3CBaggagePropagator() - const traceContextOptionsPropagator = new sdk.SwTraceContextOptionsPropagator( - diag.createComponentLogger({ namespace: "[sw/propagator]" }), - ) - const propagator = new CompositePropagator({ - propagators: [traceContextOptionsPropagator, baggagePropagator], - }) - const provider = new NodeTracerProvider({ - sampler: new ParentBasedSampler({ - root: sampler, - remoteParentSampled: sampler, - remoteParentNotSampled: sampler, - }), + sampler: sampler(config), resource, }) - if (config.experimental.swTraces) { - provider.addSpanProcessor(spanProcessor) - } - if (config.experimental.otlpTraces) { - const { OTLPTraceExporter } = await import( - "@opentelemetry/exporter-trace-otlp-grpc" - ) - provider.addSpanProcessor( - new BatchSpanProcessor( - // configurable through standard OTel environment - new OTLPTraceExporter(), - ), - ) + const processors = await spanProcessors(config, reporter) + for (const processor of processors) { + provider.addSpanProcessor(processor) } - provider.register({ propagator }) + provider.register({ propagator: propagator() }) return provider } @@ -201,36 +167,15 @@ async function initMetrics( reporter: oboe.Reporter, logger: DiagLogger, ) { - const exporter = new sdk.SwMetricsExporter( - reporter, - diag.createComponentLogger({ namespace: "[sw/metrics]" }), - ) - - const reader = new PeriodicExportingMetricReader({ - exporter, - exportIntervalMillis: config.metrics.interval, - }) - const provider = new MeterProvider({ resource, views: config.metrics.views, }) - if (config.experimental.swMetrics) { + const readers = await metricReaders(config, reporter) + for (const reader of readers) { provider.addMetricReader(reader) } - if (config.experimental.otlpMetrics) { - const { OTLPMetricExporter } = await import( - "@opentelemetry/exporter-metrics-otlp-grpc" - ) - provider.addMetricReader( - new PeriodicExportingMetricReader({ - // configurable through standard OTel environment - exporter: new OTLPMetricExporter(), - exportIntervalMillis: config.metrics.interval, - }), - ) - } metrics.setGlobalMeterProvider(provider) @@ -248,13 +193,110 @@ async function initMetrics( return provider } -async function initMessage(resource: Resource, reporter: oboe.Reporter) { +async function initMessage( + config: ExtendedSwConfiguration, + resource: Resource, + reporter: oboe.Reporter, +) { + if (!config.dev.initMessage) return + if (resource.asyncAttributesPending) { await resource.waitForAsyncAttributes?.() } sdk.sendStatus(reporter, await sdk.initMessage(resource, version)) } +function sampler(config: ExtendedSwConfiguration): Sampler { + const sampler = new sdk.SwSampler( + config, + diag.createComponentLogger({ namespace: "[sw/sampler]" }), + IS_SERVERLESS ? new oboe.OboeAPI() : undefined, + ) + return new ParentBasedSampler({ + root: sampler, + remoteParentSampled: sampler, + remoteParentNotSampled: sampler, + }) +} + +function propagator(): TextMapPropagator { + const baggagePropagator = new W3CBaggagePropagator() + const traceContextOptionsPropagator = new sdk.SwTraceContextOptionsPropagator( + diag.createComponentLogger({ namespace: "[sw/propagator]" }), + ) + return new CompositePropagator({ + propagators: [traceContextOptionsPropagator, baggagePropagator], + }) +} + +async function spanProcessors( + config: ExtendedSwConfiguration, + reporter: oboe.Reporter, +): Promise { + const processors: SpanProcessor[] = [] + + const parentInfoProcessor = new sdk.SwParentInfoSpanProcessor() + const inboundMetricsProcessor = new sdk.SwInboundMetricsSpanProcessor() + + if (config.dev.swTraces) { + const exporter = new sdk.SwExporter( + reporter, + diag.createComponentLogger({ namespace: "[sw/exporter]" }), + ) + processors.push( + new sdk.CompoundSpanProcessor(exporter, [ + parentInfoProcessor, + inboundMetricsProcessor, + ]), + ) + } + + if (config.dev.otlpTraces) { + const { SwOtlpExporter } = await import("@solarwinds-apm/sdk/otlp-exporter") + const exporter = new SwOtlpExporter() + processors.push( + new sdk.CompoundSpanProcessor(exporter, [parentInfoProcessor]), + ) + } + + return processors +} + +async function metricReaders( + config: ExtendedSwConfiguration, + reporter: oboe.Reporter, +): 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 { OTLPMetricExporter } = await import( + "@opentelemetry/exporter-metrics-otlp-grpc" + ) + const exporter = new OTLPMetricExporter() + readers.push( + new PeriodicExportingMetricReader({ + exporter, + exportIntervalMillis: config.metrics.interval, + }), + ) + } + + return readers +} + export function oboeLevelToOtelLogger( level: number, logger: DiagLogger, diff --git a/tsconfig.base.json b/tsconfig.base.json index 397102ff..a48c31eb 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -5,6 +5,7 @@ "module": "node16", "moduleResolution": "node16", "declaration": true, + "declarationMap": true, "sourceMap": true, "esModuleInterop": true, "allowJs": true, diff --git a/yarn.lock b/yarn.lock index c34c7e44..7c3703e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1795,6 +1795,7 @@ __metadata: "@solarwinds-apm/bindings-linux-x64-gnu-serverless": "workspace:*" "@solarwinds-apm/bindings-linux-x64-musl": "workspace:*" "@solarwinds-apm/eslint-config": "workspace:^" + "@solarwinds-apm/module": "workspace:^" "@solarwinds-apm/test": "workspace:^" "@types/node": "npm:^16.13.0" eslint: "npm:^8.50.0" @@ -2124,6 +2125,7 @@ __metadata: "@solarwinds-apm/eslint-config": "workspace:^" "@solarwinds-apm/histogram": "workspace:^" "@solarwinds-apm/lazy": "workspace:^" + "@solarwinds-apm/module": "workspace:^" "@solarwinds-apm/test": "workspace:^" "@types/node": "npm:^16.13.0" "@types/semver": "npm:^7.5.3" @@ -2133,9 +2135,12 @@ __metadata: typescript: "npm:^5.2.2" peerDependencies: "@opentelemetry/api": ^1.3.0 + "@opentelemetry/exporter-trace-otlp-grpc": ">=0.34.0" peerDependenciesMeta: "@opentelemetry/api": optional: false + "@opentelemetry/exporter-trace-otlp-grpc": + optional: true languageName: unknown linkType: soft @@ -7230,11 +7235,13 @@ __metadata: "@opentelemetry/instrumentation": "npm:~0.45.1" "@opentelemetry/resources": "npm:~1.18.1" "@opentelemetry/sdk-metrics": "npm:~1.18.1" + "@opentelemetry/sdk-trace-base": "npm:~1.18.1" "@opentelemetry/sdk-trace-node": "npm:~1.18.1" "@opentelemetry/semantic-conventions": "npm:~1.18.1" "@solarwinds-apm/bindings": "workspace:^" "@solarwinds-apm/eslint-config": "workspace:^" "@solarwinds-apm/instrumentations": "workspace:^" + "@solarwinds-apm/lazy": "workspace:^" "@solarwinds-apm/module": "workspace:^" "@solarwinds-apm/rollup-config": "workspace:^" "@solarwinds-apm/sdk": "workspace:^" From 18815b2446b8dc2976acc3e9759dbdc2eb41957b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Wed, 15 Nov 2023 16:41:31 -0800 Subject: [PATCH 2/6] add todo --- packages/solarwinds-apm/src/init.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/solarwinds-apm/src/init.ts b/packages/solarwinds-apm/src/init.ts index dc0fc95e..b18b289c 100644 --- a/packages/solarwinds-apm/src/init.ts +++ b/packages/solarwinds-apm/src/init.ts @@ -255,6 +255,7 @@ async function spanProcessors( const { SwOtlpExporter } = await import("@solarwinds-apm/sdk/otlp-exporter") const exporter = new SwOtlpExporter() processors.push( + // TODO: inboundMetricsProcessor replacement ? is it even necessary here ? new sdk.CompoundSpanProcessor(exporter, [parentInfoProcessor]), ) } From 231d3965a754415c3e8436a2329ea40632e35a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Wed, 15 Nov 2023 16:42:09 -0800 Subject: [PATCH 3/6] mark version bumps --- .yarn/versions/450733cf.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .yarn/versions/450733cf.yml diff --git a/.yarn/versions/450733cf.yml b/.yarn/versions/450733cf.yml new file mode 100644 index 00000000..c2241ecb --- /dev/null +++ b/.yarn/versions/450733cf.yml @@ -0,0 +1,9 @@ +releases: + "@solarwinds-apm/bindings": minor + "@solarwinds-apm/module": minor + "@solarwinds-apm/sdk": minor + solarwinds-apm: minor + +declined: + - "@solarwinds-apm/dependencies" + - "@solarwinds-apm/test" From baa50797daa3609a5cd8f973e2690739f09c3e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Wed, 15 Nov 2023 16:55:07 -0800 Subject: [PATCH 4/6] fix some init work --- packages/solarwinds-apm/package.json | 1 - packages/solarwinds-apm/src/config.ts | 1 - packages/solarwinds-apm/src/init.ts | 36 ++++++++++++++++----------- yarn.lock | 1 - 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/solarwinds-apm/package.json b/packages/solarwinds-apm/package.json index a7adb89f..d4f66587 100644 --- a/packages/solarwinds-apm/package.json +++ b/packages/solarwinds-apm/package.json @@ -65,7 +65,6 @@ "@opentelemetry/semantic-conventions": "~1.18.1", "@solarwinds-apm/bindings": "workspace:^", "@solarwinds-apm/instrumentations": "workspace:^", - "@solarwinds-apm/lazy": "workspace:^", "@solarwinds-apm/module": "workspace:^", "@solarwinds-apm/sdk": "workspace:^", "semver": "^7.5.4", diff --git a/packages/solarwinds-apm/src/config.ts b/packages/solarwinds-apm/src/config.ts index 1468d0bb..c7f32540 100644 --- a/packages/solarwinds-apm/src/config.ts +++ b/packages/solarwinds-apm/src/config.ts @@ -167,7 +167,6 @@ const schema = z.object({ otlpMetrics: boolean.default(IS_SERVERLESS), swTraces: boolean.default(!IS_SERVERLESS), swMetrics: boolean.default(!IS_SERVERLESS), - serverlessSampling: boolean.default(IS_SERVERLESS), initMessage: boolean.default(!IS_SERVERLESS), }) .default({}), diff --git a/packages/solarwinds-apm/src/init.ts b/packages/solarwinds-apm/src/init.ts index b18b289c..ed5d5f7f 100644 --- a/packages/solarwinds-apm/src/init.ts +++ b/packages/solarwinds-apm/src/init.ts @@ -41,7 +41,6 @@ import { getDetectedResource, getInstrumentations, } from "@solarwinds-apm/instrumentations" -import { lazy } from "@solarwinds-apm/lazy" import { IS_SERVERLESS } from "@solarwinds-apm/module" import * as sdk from "@solarwinds-apm/sdk" @@ -95,7 +94,10 @@ export async function init() { [SemanticResourceAttributes.SERVICE_NAME]: config.serviceName, }), ) - const reporter = lazy(() => sdk.createReporter(config)) + + const [reporter, serverlessApi] = IS_SERVERLESS + ? [undefined, new oboe.OboeAPI()] + : [sdk.createReporter(config), undefined] oboe.debug_log_add((module, level, sourceName, sourceLine, message) => { const logger = diag.createComponentLogger({ @@ -112,8 +114,8 @@ export async function init() { }, config.oboeLogLevel) const [tracerProvider, meterProvider] = await Promise.all([ - initTracing(config, resource, reporter), - initMetrics(config, resource, reporter, logger), + initTracing(config, resource, reporter, serverlessApi), + initMetrics(config, resource, logger, reporter), initMessage(config, resource, reporter), ]) registerInstrumentations(tracerProvider, meterProvider) @@ -144,10 +146,11 @@ function initInstrumentations(config: ExtendedSwConfiguration) { async function initTracing( config: ExtendedSwConfiguration, resource: Resource, - reporter: oboe.Reporter, + reporter: oboe.Reporter | undefined, + serverlessApi: oboe.OboeAPI | undefined, ) { const provider = new NodeTracerProvider({ - sampler: sampler(config), + sampler: sampler(config, serverlessApi), resource, }) @@ -164,8 +167,8 @@ async function initTracing( async function initMetrics( config: ExtendedSwConfiguration, resource: Resource, - reporter: oboe.Reporter, logger: DiagLogger, + reporter: oboe.Reporter | undefined, ) { const provider = new MeterProvider({ resource, @@ -196,9 +199,9 @@ async function initMetrics( async function initMessage( config: ExtendedSwConfiguration, resource: Resource, - reporter: oboe.Reporter, + reporter: oboe.Reporter | undefined, ) { - if (!config.dev.initMessage) return + if (!config.dev.initMessage || !reporter) return if (resource.asyncAttributesPending) { await resource.waitForAsyncAttributes?.() @@ -206,11 +209,14 @@ async function initMessage( sdk.sendStatus(reporter, await sdk.initMessage(resource, version)) } -function sampler(config: ExtendedSwConfiguration): Sampler { +function sampler( + config: ExtendedSwConfiguration, + serverlessApi: oboe.OboeAPI | undefined, +): Sampler { const sampler = new sdk.SwSampler( config, diag.createComponentLogger({ namespace: "[sw/sampler]" }), - IS_SERVERLESS ? new oboe.OboeAPI() : undefined, + serverlessApi, ) return new ParentBasedSampler({ root: sampler, @@ -231,7 +237,7 @@ function propagator(): TextMapPropagator { async function spanProcessors( config: ExtendedSwConfiguration, - reporter: oboe.Reporter, + reporter: oboe.Reporter | undefined, ): Promise { const processors: SpanProcessor[] = [] @@ -240,7 +246,7 @@ async function spanProcessors( if (config.dev.swTraces) { const exporter = new sdk.SwExporter( - reporter, + reporter!, diag.createComponentLogger({ namespace: "[sw/exporter]" }), ) processors.push( @@ -265,13 +271,13 @@ async function spanProcessors( async function metricReaders( config: ExtendedSwConfiguration, - reporter: oboe.Reporter, + reporter: oboe.Reporter | undefined, ): Promise { const readers: MetricReader[] = [] if (config.dev.swMetrics) { const exporter = new sdk.SwMetricsExporter( - reporter, + reporter!, diag.createComponentLogger({ namespace: "[sw/metrics]" }), ) readers.push( diff --git a/yarn.lock b/yarn.lock index 7c3703e0..8385272a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7241,7 +7241,6 @@ __metadata: "@solarwinds-apm/bindings": "workspace:^" "@solarwinds-apm/eslint-config": "workspace:^" "@solarwinds-apm/instrumentations": "workspace:^" - "@solarwinds-apm/lazy": "workspace:^" "@solarwinds-apm/module": "workspace:^" "@solarwinds-apm/rollup-config": "workspace:^" "@solarwinds-apm/sdk": "workspace:^" From f1dc95feb51cc28ac70ede6678f1f67a5b424427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Wed, 15 Nov 2023 16:58:14 -0800 Subject: [PATCH 5/6] missing rename --- packages/solarwinds-apm/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/solarwinds-apm/src/config.ts b/packages/solarwinds-apm/src/config.ts index c7f32540..46bdad5d 100644 --- a/packages/solarwinds-apm/src/config.ts +++ b/packages/solarwinds-apm/src/config.ts @@ -200,7 +200,7 @@ export function readConfig(): ExtendedSwConfiguration { const raw = schema.parse({ ...file, ...env, - experimental: { ...devFile, ...devEnv }, + dev: { ...devFile, ...devEnv }, }) const config: ExtendedSwConfiguration = { From 79f6f754adf65a45d9faa6d6c9e22c256c1a5ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Wed, 15 Nov 2023 17:01:29 -0800 Subject: [PATCH 6/6] fix config tests --- packages/solarwinds-apm/test/config.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/solarwinds-apm/test/config.test.ts b/packages/solarwinds-apm/test/config.test.ts index 01da585a..a9bd07b6 100644 --- a/packages/solarwinds-apm/test/config.test.ts +++ b/packages/solarwinds-apm/test/config.test.ts @@ -43,11 +43,12 @@ describe("readConfig", () => { insertTraceContextIntoQueries: false, instrumentations: {}, metrics: { interval: 60_000 }, - experimental: { + dev: { otlpTraces: false, otlpMetrics: false, swTraces: true, swMetrics: true, + initMessage: true, }, } @@ -83,13 +84,13 @@ describe("readConfig", () => { expect(config.transactionSettings).to.have.length(3) }) - it("parses experimental env", () => { - process.env.SW_APM_EXPERIMENTAL_OTLP_TRACES = "true" - process.env.SW_APM_EXPERIMENTAL_SW_METRICS = "0" + it("parses dev env", () => { + process.env.SW_APM_DEV_OTLP_TRACES = "true" + process.env.SW_APM_DEV_SW_METRICS = "0" const config = readConfig() - expect(config.experimental.otlpTraces).to.be.true - expect(config.experimental.swMetrics).to.be.false + expect(config.dev.otlpTraces).to.be.true + expect(config.dev.swMetrics).to.be.false }) it("throws on bad boolean", () => {