Skip to content

Commit 834cf09

Browse files
authored
Add support for sampling for node. (#90)
## Summary Adds support for export sampling for the node observability SDK. ## How did you test this change? <!-- Frontend - Leave a screencast or a screenshot to visually describe the changes. --> ## Are there any deployment considerations? <!-- Backend - Do we need to consider migrations or backfilling data? --> ## Does this work require review from our design team? <!-- Request review from julian-highlight / our design team -->
1 parent cb4fd40 commit 834cf09

22 files changed

+2966
-231
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { type CodegenConfig } from '@graphql-codegen/cli'
2+
3+
const config: CodegenConfig = {
4+
schema: '../../../../backend/public-graph/graph/schema.graphqls',
5+
documents: ['src/**/*.gql'],
6+
generates: {
7+
'./src/graph/generated/': {
8+
preset: 'client',
9+
config: {
10+
documentMode: 'string',
11+
},
12+
},
13+
},
14+
}
15+
export default config

sdk/@launchdarkly/observability-node/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"typegen": "tsc -d --emitDeclarationOnly",
1111
"dev": "yarn build --watch",
1212
"build": "rollup --config rollup.config.mjs",
13-
"test": "vitest run"
13+
"test": "vitest run",
14+
"codegen": "graphql-codegen"
1415
},
1516
"main": "./dist/index.cjs",
1617
"module": "./dist/index.js",
@@ -26,6 +27,7 @@
2627
"access": "public"
2728
},
2829
"dependencies": {
30+
"@graphql-codegen/cli": "^5.0.7",
2931
"@launchdarkly/node-server-sdk-otel": "^1.2.2",
3032
"@prisma/instrumentation": ">=5.0.0",
3133
"require-in-the-middle": "^7.4.0"

sdk/@launchdarkly/observability-node/src/api/Options.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export interface NodeOptions {
3030
*/
3131
otlpEndpoint?: string
3232

33+
/**
34+
* Specifies the URL used for non-OTLP operations.
35+
* These include accessing client sampling configuration.
36+
*/
37+
backendUrl?: string
38+
3339
/**
3440
* This app's service name.
3541
*/

sdk/@launchdarkly/observability-node/src/client/ObservabilityClient.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ import {
2626
W3CBaggagePropagator,
2727
W3CTraceContextPropagator,
2828
} from '@opentelemetry/core'
29-
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
30-
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'
3129
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
3230
import {
3331
type Instrumentation,
@@ -61,8 +59,13 @@ import { NodeOptions } from '../api/Options.js'
6159
import { Metric } from '../api/Metric.js'
6260
import { RequestContext } from '../api/RequestContext.js'
6361
import { Headers, IncomingHttpHeaders } from '../api/headers.js'
62+
import { getSamplingConfig } from '../graph/getSamplingConfig.js'
63+
import { SamplingTraceExporter } from '../otel/SamplingTraceExporter.js'
64+
import { SamplingLogExporter } from '../otel/SamplingLogExporter.js'
65+
import { CustomSampler } from '../otel/sampling/CustomSampler.js'
6466

6567
const OTLP_HTTP = 'https://otel.observability.app.launchdarkly.com:4318'
68+
const BACKEND_URL = 'https://pub.observability.app.launchdarkly.com'
6669
export const HIGHLIGHT_REQUEST_HEADER = 'x-highlight-request'
6770

6871
const instrumentations = getNodeAutoInstrumentations({
@@ -106,6 +109,7 @@ export class ObservabilityClient {
106109
_projectID: string
107110
_debug: boolean
108111
otel: NodeSDK
112+
private readonly backendUrl: string
109113

110114
private readonly tracer: Tracer
111115
private readonly logger: Logger
@@ -133,6 +137,7 @@ export class ObservabilityClient {
133137
const optionsWithDebug = options as OptionsWithDebug
134138
this._debug = !!optionsWithDebug.debug
135139
this._projectID = sdkKey
140+
this.backendUrl = options.backendUrl ?? BACKEND_URL
136141

137142
if (!this._projectID) {
138143
console.warn(
@@ -213,19 +218,27 @@ export class ObservabilityClient {
213218
}
214219
const resource = new Resource(attributes)
215220

216-
const exporter = new OTLPTraceExporter({
217-
...config,
218-
url: `${config.url}/v1/traces`,
219-
})
221+
const sampler = new CustomSampler()
222+
this._getSamplingConfig(sampler)
223+
const exporter = new SamplingTraceExporter(
224+
{
225+
...config,
226+
url: `${config.url}/v1/traces`,
227+
},
228+
sampler,
229+
)
220230
this.processor = new BatchSpanProcessor(exporter, opts)
221231

222232
this.loggerProvider = new LoggerProvider({
223233
resource,
224234
})
225-
const logsExporter = new OTLPLogExporter({
226-
...config,
227-
url: `${config.url}/v1/logs`,
228-
})
235+
const logsExporter = new SamplingLogExporter(
236+
{
237+
...config,
238+
url: `${config.url}/v1/logs`,
239+
},
240+
sampler,
241+
)
229242
const logsProcessor = new BatchLogRecordProcessor(logsExporter, opts)
230243
this.loggerProvider.addLogRecordProcessor(logsProcessor)
231244

@@ -291,6 +304,18 @@ export class ObservabilityClient {
291304
this._log(`Initialized SDK for project ${this._projectID}`)
292305
}
293306

307+
private async _getSamplingConfig(sampler: CustomSampler) {
308+
try {
309+
const result = await getSamplingConfig(
310+
this.backendUrl,
311+
this._projectID,
312+
)
313+
sampler.setConfig(result)
314+
} catch (err) {
315+
this._log('failed to get sampling config: ', err)
316+
}
317+
}
318+
294319
async stop() {
295320
await this.flush()
296321
await this.otel.shutdown()
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/* eslint-disable */
2+
import {
3+
ResultOf,
4+
DocumentTypeDecoration,
5+
} from '@graphql-typed-document-node/core'
6+
import { Incremental, TypedDocumentString } from './graphql'
7+
8+
export type FragmentType<
9+
TDocumentType extends DocumentTypeDecoration<any, any>,
10+
> =
11+
TDocumentType extends DocumentTypeDecoration<infer TType, any>
12+
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
13+
? TKey extends string
14+
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
15+
: never
16+
: never
17+
: never
18+
19+
// return non-nullable if `fragmentType` is non-nullable
20+
export function useFragment<TType>(
21+
_documentNode: DocumentTypeDecoration<TType, any>,
22+
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>,
23+
): TType
24+
// return nullable if `fragmentType` is undefined
25+
export function useFragment<TType>(
26+
_documentNode: DocumentTypeDecoration<TType, any>,
27+
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | undefined,
28+
): TType | undefined
29+
// return nullable if `fragmentType` is nullable
30+
export function useFragment<TType>(
31+
_documentNode: DocumentTypeDecoration<TType, any>,
32+
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null,
33+
): TType | null
34+
// return nullable if `fragmentType` is nullable or undefined
35+
export function useFragment<TType>(
36+
_documentNode: DocumentTypeDecoration<TType, any>,
37+
fragmentType:
38+
| FragmentType<DocumentTypeDecoration<TType, any>>
39+
| null
40+
| undefined,
41+
): TType | null | undefined
42+
// return array of non-nullable if `fragmentType` is array of non-nullable
43+
export function useFragment<TType>(
44+
_documentNode: DocumentTypeDecoration<TType, any>,
45+
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>>,
46+
): Array<TType>
47+
// return array of nullable if `fragmentType` is array of nullable
48+
export function useFragment<TType>(
49+
_documentNode: DocumentTypeDecoration<TType, any>,
50+
fragmentType:
51+
| Array<FragmentType<DocumentTypeDecoration<TType, any>>>
52+
| null
53+
| undefined,
54+
): Array<TType> | null | undefined
55+
// return readonly array of non-nullable if `fragmentType` is array of non-nullable
56+
export function useFragment<TType>(
57+
_documentNode: DocumentTypeDecoration<TType, any>,
58+
fragmentType: ReadonlyArray<
59+
FragmentType<DocumentTypeDecoration<TType, any>>
60+
>,
61+
): ReadonlyArray<TType>
62+
// return readonly array of nullable if `fragmentType` is array of nullable
63+
export function useFragment<TType>(
64+
_documentNode: DocumentTypeDecoration<TType, any>,
65+
fragmentType:
66+
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
67+
| null
68+
| undefined,
69+
): ReadonlyArray<TType> | null | undefined
70+
export function useFragment<TType>(
71+
_documentNode: DocumentTypeDecoration<TType, any>,
72+
fragmentType:
73+
| FragmentType<DocumentTypeDecoration<TType, any>>
74+
| Array<FragmentType<DocumentTypeDecoration<TType, any>>>
75+
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
76+
| null
77+
| undefined,
78+
): TType | Array<TType> | ReadonlyArray<TType> | null | undefined {
79+
return fragmentType as any
80+
}
81+
82+
export function makeFragmentData<
83+
F extends DocumentTypeDecoration<any, any>,
84+
FT extends ResultOf<F>,
85+
>(data: FT, _fragment: F): FragmentType<F> {
86+
return data as FragmentType<F>
87+
}
88+
export function isFragmentReady<TQuery, TFrag>(
89+
queryNode: TypedDocumentString<TQuery, any>,
90+
fragmentNode: TypedDocumentString<TFrag, any>,
91+
data:
92+
| FragmentType<TypedDocumentString<Incremental<TFrag>, any>>
93+
| null
94+
| undefined,
95+
): data is FragmentType<typeof fragmentNode> {
96+
const deferredFields = queryNode.__meta__?.deferredFields as Record<
97+
string,
98+
(keyof TFrag)[]
99+
>
100+
const fragName = fragmentNode.__meta__?.fragmentName as string | undefined
101+
102+
if (!deferredFields || !fragName) return true
103+
104+
const fields = deferredFields[fragName] ?? []
105+
return fields.length > 0 && fields.every((field) => data && field in data)
106+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* eslint-disable */
2+
import * as types from './graphql'
3+
4+
/**
5+
* Map of all GraphQL operations in the project.
6+
*
7+
* This map has several performance disadvantages:
8+
* 1. It is not tree-shakeable, so it will include all operations in the project.
9+
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
10+
* 3. It does not support dead code elimination, so it will add unused operations.
11+
*
12+
* Therefore it is highly recommended to use the babel or swc plugin for production.
13+
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
14+
*/
15+
type Documents = {
16+
'fragment MatchParts on MatchConfig {\n regexValue\n matchValue\n}\n\nquery GetSamplingConfig($organization_verbose_id: String!) {\n sampling(organization_verbose_id: $organization_verbose_id) {\n spans {\n name {\n ...MatchParts\n }\n attributes {\n key {\n ...MatchParts\n }\n attribute {\n ...MatchParts\n }\n }\n events {\n name {\n ...MatchParts\n }\n attributes {\n key {\n ...MatchParts\n }\n attribute {\n ...MatchParts\n }\n }\n }\n samplingRatio\n }\n logs {\n message {\n ...MatchParts\n }\n severityText {\n ...MatchParts\n }\n attributes {\n key {\n ...MatchParts\n }\n attribute {\n ...MatchParts\n }\n }\n samplingRatio\n }\n }\n}': typeof types.MatchPartsFragmentDoc
17+
}
18+
const documents: Documents = {
19+
'fragment MatchParts on MatchConfig {\n regexValue\n matchValue\n}\n\nquery GetSamplingConfig($organization_verbose_id: String!) {\n sampling(organization_verbose_id: $organization_verbose_id) {\n spans {\n name {\n ...MatchParts\n }\n attributes {\n key {\n ...MatchParts\n }\n attribute {\n ...MatchParts\n }\n }\n events {\n name {\n ...MatchParts\n }\n attributes {\n key {\n ...MatchParts\n }\n attribute {\n ...MatchParts\n }\n }\n }\n samplingRatio\n }\n logs {\n message {\n ...MatchParts\n }\n severityText {\n ...MatchParts\n }\n attributes {\n key {\n ...MatchParts\n }\n attribute {\n ...MatchParts\n }\n }\n samplingRatio\n }\n }\n}':
20+
types.MatchPartsFragmentDoc,
21+
}
22+
23+
/**
24+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
25+
*/
26+
export function graphql(
27+
source: 'fragment MatchParts on MatchConfig {\n regexValue\n matchValue\n}\n\nquery GetSamplingConfig($organization_verbose_id: String!) {\n sampling(organization_verbose_id: $organization_verbose_id) {\n spans {\n name {\n ...MatchParts\n }\n attributes {\n key {\n ...MatchParts\n }\n attribute {\n ...MatchParts\n }\n }\n events {\n name {\n ...MatchParts\n }\n attributes {\n key {\n ...MatchParts\n }\n attribute {\n ...MatchParts\n }\n }\n }\n samplingRatio\n }\n logs {\n message {\n ...MatchParts\n }\n severityText {\n ...MatchParts\n }\n attributes {\n key {\n ...MatchParts\n }\n attribute {\n ...MatchParts\n }\n }\n samplingRatio\n }\n }\n}',
28+
): typeof import('./graphql').MatchPartsFragmentDoc
29+
30+
export function graphql(source: string) {
31+
return (documents as any)[source] ?? {}
32+
}

0 commit comments

Comments
 (0)