Skip to content

Commit

Permalink
Implement opentelemetryInstrumentations getter
Browse files Browse the repository at this point in the history
Implement an `opentelemetryInstrumentations` getter on the client,
to allow users of the `initializeOpentelemetrySdk` config option to
configure OpenTelemetry in an AppSignal-compatible way.

This getter honors the settings in the `disableDefaultInstrumentations`
and `additionalInstrumentations` configuration options. Implementing
this was a bit of a refactor, so I took a moment to add tests for this
and the `initializeOpentelemetrySdk` config option.
  • Loading branch information
unflxw committed Oct 30, 2023
1 parent 7a21d95 commit ab7f3c7
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 31 deletions.
20 changes: 16 additions & 4 deletions .changesets/add-initialize-opentelemetry-sdk-config-option.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ Add `initializeOpentelemetrySdk` configuration option. This allows those who
would rather take control of how OpenTelemetry is initialised in their
application to skip AppSignal's initialization of the OpenTelemetry SDK.

Additionally, add an `opentelemetryInstrumentations` method on the client,
which returns AppSignal's default OpenTelemetry instrumentations, already
configured to work correctly with AppSignal. The provided list of
instrumentations will follow the `additionalInstrumentations` and
`disableDefaultInstrumentations` config options, if those are set.

This is not the recommended way to use AppSignal for Node.js. Only use this
config option if you're really sure that you know what you're doing.
config option and this method if you're really sure that you know what
you're doing.

When initialising OpenTelemetry, it is necessary to add the AppSignal span
processor in order for data to be sent to AppSignal. For example, using the
Expand All @@ -20,12 +27,17 @@ import { SpanProcessor, Appsignal } from "@appsignal/nodejs";

const sdk = new NodeSDK({
spanProcessor: new SpanProcessor(Appsignal.client)
instrumentations: Appsignal.client.opentelemetryInstrumentations()
});

sdk.start()
```

The above snippet assumes that the AppSignal client has been initialised
beforehand. For an optimal experience with AppSignal, those making use of this
config option should configure their OpenTelemetry instrumentation in a
similar way as it is done in the AppSignal integration's source code.
beforehand.

When making use of this config option, the OpenTelemetry instrumentations
must be configured in the same way as it is done in the AppSignal integration.
In the above snippet, the `instrumentations` property in the OpenTelemetry SDK
is set to the AppSignal client's list of OpenTelemetry instrumentations, which
are configured to work correctly with AppSignal.
64 changes: 53 additions & 11 deletions src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { InstrumentationTestRegistry } from "../../test/instrumentation_registry"
import {
InstrumentationTestRegistry,
instrumentationNames
} from "../../test/instrumentation_registry"
import { Extension } from "../extension"
import { Client } from "../client"
import { Metrics } from "../metrics"
Expand All @@ -16,6 +19,7 @@ describe("Client", () => {
const DEFAULT_OPTS = { name, pushApiKey }

beforeEach(() => {
InstrumentationTestRegistry.clear()
client = new Client({ ...DEFAULT_OPTS })
})

Expand Down Expand Up @@ -142,16 +146,37 @@ describe("Client", () => {
expect(meter).toBeInstanceOf(Metrics)
})

it("initializes the OpenTelemetry SDK when active", () => {
client = new Client({ ...DEFAULT_OPTS, active: true })
expect(InstrumentationTestRegistry.didInitializeSDK).toEqual(true)
})

it("does not initialize the OpenTelemetry SDK when its config option is false", () => {
client = new Client({
...DEFAULT_OPTS,
active: true,
initializeOpentelemetrySdk: false
})
expect(InstrumentationTestRegistry.didInitializeSDK).toEqual(false)
})

describe("Instrumentations", () => {
it("registers the default instrumentations", () => {
client = new Client({ ...DEFAULT_OPTS, active: true })
// Not testing against all of them or a fixed number so
// that we don't have to change these tests every time we
// add a new instrumentation.
const instrumentationNames =
const registryInstrumentationNames =
InstrumentationTestRegistry.instrumentationNames()
expect(instrumentationNames.length).toBeGreaterThan(10)
expect(instrumentationNames).toContain(
expect(registryInstrumentationNames.length).toBeGreaterThan(10)
expect(registryInstrumentationNames).toContain(
"@opentelemetry/instrumentation-http"
)

const opentelemetryInstrumentations =
client.opentelemetryInstrumentations()
expect(opentelemetryInstrumentations.length).toBeGreaterThan(10)
expect(instrumentationNames(opentelemetryInstrumentations)).toContain(
"@opentelemetry/instrumentation-http"
)
})
Expand All @@ -165,6 +190,7 @@ describe("Client", () => {
const instrumentationNames =
InstrumentationTestRegistry.instrumentationNames()
expect(instrumentationNames).toEqual([])
expect(client.opentelemetryInstrumentations()).toEqual([])
})

it("can disable some default instrumentations", () => {
Expand All @@ -173,12 +199,19 @@ describe("Client", () => {
active: true,
disableDefaultInstrumentations: ["@opentelemetry/instrumentation-http"]
})
const instrumentationNames =
const registryInstrumentationNames =
InstrumentationTestRegistry.instrumentationNames()
expect(instrumentationNames).not.toContain(
expect(registryInstrumentationNames).not.toContain(
"@opentelemetry/instrumentation-http"
)
expect(registryInstrumentationNames.length).toBeGreaterThan(10)

const opentelemetryInstrumentations =
client.opentelemetryInstrumentations()
expect(instrumentationNames(opentelemetryInstrumentations)).not.toContain(
"@opentelemetry/instrumentation-http"
)
expect(instrumentationNames.length).toBeGreaterThan(0)
expect(opentelemetryInstrumentations.length).toBeGreaterThan(10)
})

it("can add additional instrumentations", () => {
Expand Down Expand Up @@ -211,13 +244,22 @@ describe("Client", () => {
additionalInstrumentations: [new TestInstrumentation()]
})

const instrumentationNames =
const registryInstrumentationNames =
InstrumentationTestRegistry.instrumentationNames()
expect(instrumentationNames).toContain(
expect(registryInstrumentationNames).toContain(
"@opentelemetry/instrumentation-http"
)
expect(registryInstrumentationNames.length).toBeGreaterThan(10)
expect(registryInstrumentationNames).toContain("test/instrumentation")
const opentelemetryInstrumentations =
client.opentelemetryInstrumentations()
expect(instrumentationNames(opentelemetryInstrumentations)).toContain(
"@opentelemetry/instrumentation-http"
)
expect(instrumentationNames.length).toBeGreaterThan(10)
expect(instrumentationNames).toContain("test/instrumentation")
expect(opentelemetryInstrumentations.length).toBeGreaterThan(10)
expect(instrumentationNames(opentelemetryInstrumentations)).toContain(
"test/instrumentation"
)
})
})
})
25 changes: 14 additions & 11 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ type DefaultInstrumentationsConfigMap = {
>
}

type AdditionalInstrumentationsOption = NodeSDKConfiguration["instrumentations"]
type NodeSDKInstrumentationsOption = NodeSDKConfiguration["instrumentations"]

export type Options = AppsignalOptions & {
additionalInstrumentations: AdditionalInstrumentationsOption
additionalInstrumentations: NodeSDKInstrumentationsOption
}

/**
Expand All @@ -93,6 +93,7 @@ export class Client {

#metrics: Metrics
#sdk?: NodeSDK
#additionalInstrumentations: NodeSDKInstrumentationsOption

/**
* Global accessors for the AppSignal client
Expand Down Expand Up @@ -138,6 +139,8 @@ export class Client {
* Creates a new instance of the `Appsignal` object
*/
constructor(options: Partial<Options> = {}) {
this.#additionalInstrumentations = options.additionalInstrumentations || []

this.config = new Configuration(options)
this.extension = new Extension()
this.integrationLogger = this.setUpIntegrationLogger()
Expand All @@ -153,9 +156,7 @@ export class Client {
this.start()
this.#metrics = new Metrics()
if (this.config.data.initializeOpentelemetrySdk) {
this.#sdk = this.initOpenTelemetry(
options.additionalInstrumentations || []
)
this.#sdk = this.initOpenTelemetry()
}
}
} else {
Expand Down Expand Up @@ -364,12 +365,16 @@ export class Client {
)
}

public opentelemetryInstrumentations(): NodeSDKInstrumentationsOption {
return this.#additionalInstrumentations.concat(
this.defaultInstrumentations()
)
}

/**
* Initialises OpenTelemetry instrumentation
*/
private initOpenTelemetry(
additionalInstrumentations: AdditionalInstrumentationsOption
) {
private initOpenTelemetry() {
const testMode = process.env["_APPSIGNAL_TEST_MODE"]
const testModeFilePath = process.env["_APPSIGNAL_TEST_MODE_FILE_PATH"]
let spanProcessor
Expand All @@ -380,9 +385,7 @@ export class Client {
spanProcessor = new SpanProcessor(this)
}

const instrumentations = additionalInstrumentations.concat(
this.defaultInstrumentations()
)
const instrumentations = this.opentelemetryInstrumentations()

const sdk = new NodeSDK({
instrumentations,
Expand Down
14 changes: 10 additions & 4 deletions test/instrumentation_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,28 @@ import { Instrumentation } from "@opentelemetry/instrumentation"

type InstrumentationsOption = NodeSDKConfiguration["instrumentations"]

export function instrumentationNames(instrumentations: InstrumentationsOption) {
return instrumentations.map(
instrumentation => (instrumentation as Instrumentation).instrumentationName
)
}

export class InstrumentationTestRegistry {
static instrumentations?: InstrumentationsOption
static didInitializeSDK = false

static setInstrumentations(instrumentations: InstrumentationsOption) {
this.instrumentations = instrumentations
this.didInitializeSDK = true
}

static instrumentationNames() {
return (this.instrumentations || []).map(
instrumentation =>
(instrumentation as Instrumentation).instrumentationName
)
return instrumentationNames(this.instrumentations || [])
}

static clear() {
this.instrumentations = undefined
this.didInitializeSDK = false
}
}

Expand Down
2 changes: 1 addition & 1 deletion test/integration/diagnose

0 comments on commit ab7f3c7

Please sign in to comment.