Skip to content

Commit b007bc1

Browse files
authored
Add otlp exporter support behind config option (#745)
1 parent 99d7ae2 commit b007bc1

17 files changed

+752
-337
lines changed

package-lock.json

+342-186
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
"name": "@splunk/otel-web-dev-root",
33
"private": true,
44
"version": "0.17.0-beta.1",
5+
"--workspaces": "Hardcoded so npm runs workspaces commands in order",
56
"workspaces": [
6-
"packages/*"
7+
"packages/web",
8+
"packages/session-recorder"
79
],
810
"engines": {
911
"--": "Versions required for development only (recent enough to use npm workspaces)",

packages/session-recorder/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@
3232
],
3333
"dependencies": {
3434
"@babel/runtime": "~7.22.6",
35-
"@opentelemetry/api": "^1.6.0",
36-
"@opentelemetry/core": "^1.17.1",
37-
"@opentelemetry/resources": "^1.17.1",
35+
"@opentelemetry/api": "^1.8.0",
36+
"@opentelemetry/core": "^1.22.0",
37+
"@opentelemetry/resources": "^1.22.0",
3838
"fflate": "^0.8.0",
3939
"protobufjs": "~7.2.4",
4040
"rrweb": "^1.1.3",

packages/session-recorder/src/OTLPLogExporter.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { Resource } from '@opentelemetry/resources';
1817
import { gzipSync } from 'fflate';
1918
import type { Root } from 'protobufjs';
2019
import type { JsonArray, JsonObject, JsonValue } from 'type-fest';
@@ -26,7 +25,7 @@ import { VERSION } from './version.js';
2625
interface OTLPLogExporterConfig {
2726
headers?: Record<string, string>;
2827
beaconUrl: string;
29-
resource: Resource;
28+
getResourceAttributes: () => JsonObject;
3029
debug?: boolean;
3130
}
3231

@@ -98,7 +97,7 @@ export default class OTLPLogExporter {
9897
return {
9998
resourceLogs: [{
10099
resource: {
101-
attributes: convertToAnyValue(this.config.resource?.attributes || {}).kvlistValue.values,
100+
attributes: convertToAnyValue(this.config.getResourceAttributes() || {}).kvlistValue!.values,
102101
},
103102
scopeLogs: [{
104103
scope: { name: 'splunk.rr-web', version: VERSION },

packages/session-recorder/src/index.ts

+21-8
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ import { record } from 'rrweb';
1919
import OTLPLogExporter from './OTLPLogExporter';
2020
import { BatchLogProcessor, convert } from './BatchLogProcessor';
2121
import { VERSION } from './version';
22+
import { getGlobal } from './utils';
2223

2324
import type { Resource } from '@opentelemetry/resources';
24-
25+
import type { SplunkOtelWebType } from '@splunk/otel-web';
2526

2627
interface BasicTracerProvider extends TracerProvider {
2728
readonly resource: Resource;
@@ -110,16 +111,18 @@ const SplunkRumRecorder = {
110111
return;
111112
}
112113

114+
const SplunkRum = getGlobal<SplunkOtelWebType>('splunk.rum');
115+
113116
let tracerProvider: BasicTracerProvider | ProxyTracerProvider = trace.getTracerProvider() as BasicTracerProvider;
114117
if (tracerProvider && 'getDelegate' in tracerProvider) {
115118
tracerProvider = (tracerProvider as unknown as ProxyTracerProvider).getDelegate() as BasicTracerProvider;
116119
}
117-
if (!(tracerProvider?.resource)) {
118-
console.error('Splunk OTEL Web must be inited before recorder.');
120+
if (!SplunkRum || !SplunkRum.resource) {
121+
console.error('Splunk OTEL Web must be inited before session recorder.');
119122
return;
120123
}
121124

122-
const resource = tracerProvider.resource;
125+
const resource = SplunkRum.resource;
123126

124127
migrateConfig(config);
125128

@@ -156,10 +159,20 @@ const SplunkRumRecorder = {
156159
exportUrl += `?auth=${rumAccessToken}`;
157160
}
158161

159-
const exporter = new OTLPLogExporter({ beaconUrl: exportUrl, debug, headers, resource });
162+
const exporter = new OTLPLogExporter({
163+
beaconUrl: exportUrl,
164+
debug,
165+
headers,
166+
getResourceAttributes() {
167+
return {
168+
...resource.attributes,
169+
'splunk.rumSessionId': SplunkRum.getSessionId()!
170+
};
171+
}
172+
});
160173
const processor = new BatchLogProcessor(exporter, {});
161174

162-
lastKnownSession = resource.attributes['splunk.rumSessionId'] as string;
175+
lastKnownSession = SplunkRum.getSessionId() as string;
163176
sessionStartTime = Date.now();
164177

165178
inited = record({
@@ -174,11 +187,11 @@ const SplunkRumRecorder = {
174187
// Safeguards from our ingest getting DDOSed:
175188
// 1. A session can send up to 4 hours of data
176189
// 2. Recording resumes on session change if it isn't a background tab (session regenerated in an another tab)
177-
if (resource.attributes['splunk.rumSessionId'] !== lastKnownSession) {
190+
if (SplunkRum.getSessionId() !== lastKnownSession) {
178191
if (document.hidden) {
179192
return;
180193
}
181-
lastKnownSession = resource.attributes['splunk.rumSessionId'] as string;
194+
lastKnownSession = SplunkRum.getSessionId() as string;
182195
sessionStartTime = Date.now();
183196
// reset counters
184197
eventCounter = 1;
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
Copyright 2024 Splunk Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { VERSION } from './version';
18+
19+
const GLOBAL_OPENTELEMETRY_API_KEY = Symbol.for('opentelemetry.js.api.1');
20+
/**
21+
* otel-api's global function. This isn't exported by otel/api but would be
22+
* super useful to access global components for experimental purposes...
23+
* For us, it's included to access components accessed by other packages,
24+
* eg. sharing session id manager with session recorder
25+
*/
26+
export function getGlobal<Type>(
27+
type: string
28+
): Type | undefined {
29+
const globalSplunkRumVersion = globalThis[GLOBAL_OPENTELEMETRY_API_KEY]?.['splunk.rum.version'];
30+
if (!globalSplunkRumVersion || globalSplunkRumVersion !== VERSION) {
31+
console.warn(`SplunkSessionRecorder: Version mismatch with SplunkRum (RUM: ${globalSplunkRumVersion}, recorder: ${VERSION})`);
32+
return undefined; // undefined for eslint
33+
}
34+
35+
return globalThis[GLOBAL_OPENTELEMETRY_API_KEY]?.[type];
36+
}

packages/web/package.json

+14-13
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,20 @@
4747
],
4848
"dependencies": {
4949
"@babel/runtime": "^7.22.6",
50-
"@opentelemetry/api": "^1.6.0",
51-
"@opentelemetry/core": "^1.17.1",
52-
"@opentelemetry/exporter-zipkin": "^1.17.1",
53-
"@opentelemetry/instrumentation": "^0.44.0",
54-
"@opentelemetry/instrumentation-document-load": "^0.33.2",
55-
"@opentelemetry/instrumentation-fetch": "^0.44.0",
56-
"@opentelemetry/instrumentation-xml-http-request": "^0.44.0",
57-
"@opentelemetry/resources": "^1.17.1",
58-
"@opentelemetry/sdk-trace-base": "^1.17.1",
59-
"@opentelemetry/sdk-trace-web": "^1.17.1",
60-
"@opentelemetry/semantic-conventions": "^1.17.1",
50+
"@opentelemetry/api": "^1.8.0",
51+
"@opentelemetry/core": "^1.22.0",
52+
"@opentelemetry/exporter-trace-otlp-http": "^0.49.1",
53+
"@opentelemetry/exporter-zipkin": "^1.22.0",
54+
"@opentelemetry/instrumentation": "^0.49.1",
55+
"@opentelemetry/instrumentation-document-load": "^0.36.0",
56+
"@opentelemetry/instrumentation-fetch": "^0.49.1",
57+
"@opentelemetry/instrumentation-xml-http-request": "^0.49.1",
58+
"@opentelemetry/resources": "^1.22.0",
59+
"@opentelemetry/sdk-trace-base": "^1.22.0",
60+
"@opentelemetry/sdk-trace-web": "^1.22.0",
61+
"@opentelemetry/semantic-conventions": "^1.22.0",
6162
"shimmer": "^1.2.1",
62-
"web-vitals": "^3.4.0"
63+
"web-vitals": "^3.5.2"
6364
},
6465
"devDependencies": {
6566
"@babel/plugin-transform-runtime": "^7.22.9",
@@ -79,7 +80,7 @@
7980
"browserstack-local": "^1.5.3",
8081
"chai": "^4.3.7",
8182
"chrome-launcher": "^1.0.0",
82-
"chromedriver": "^119.0.1",
83+
"chromedriver": "^121.0.0",
8384
"codecov": "^3.8.2",
8485
"compression": "^1.7.4",
8586
"cors": "^2.8.5",

packages/web/src/SplunkSpanAttributesProcessor.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ limitations under the License.
1616

1717
import { Attributes } from '@opentelemetry/api';
1818
import { Span, SpanProcessor } from '@opentelemetry/sdk-trace-base';
19+
import { getRumSessionId } from './session';
1920

2021
export class SplunkSpanAttributesProcessor implements SpanProcessor {
2122
private readonly _globalAttributes: Attributes;
@@ -45,6 +46,7 @@ export class SplunkSpanAttributesProcessor implements SpanProcessor {
4546
onStart(span: Span): void {
4647
span.setAttribute('location.href', location.href);
4748
span.setAttributes(this._globalAttributes);
49+
span.setAttribute('splunk.rumSessionId', getRumSessionId());
4850
}
4951

5052
onEnd(): void {

packages/web/src/exporters/common.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2024 Splunk Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { Attributes } from '@opentelemetry/api';
18+
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
19+
20+
export interface SplunkExporterConfig {
21+
url: string;
22+
onAttributesSerializing?: (attributes: Attributes, span: ReadableSpan) => Attributes,
23+
xhrSender?: (url: string, data: string, headers?: Record<string, string>) => void,
24+
beaconSender?: (url: string, data: string, headers?: Record<string, string>) => void,
25+
}
26+
27+
export function NOOP_ATTRIBUTES_TRANSFORMER(attributes: Attributes): Attributes {
28+
return attributes;
29+
}
30+
export function NATIVE_XHR_SENDER(url: string, data: string, headers?: Record<string, string>): void {
31+
const xhr = new XMLHttpRequest();
32+
xhr.open('POST', url);
33+
const defaultHeaders = {
34+
Accept: 'application/json',
35+
'Content-Type': 'application/json',
36+
};
37+
Object.entries(Object.assign(Object.assign({}, defaultHeaders), headers)).forEach(([k, v]) => {
38+
xhr.setRequestHeader(k, v);
39+
});
40+
xhr.send(data);
41+
}
42+
export function NATIVE_BEACON_SENDER(url: string, data: string, blobPropertyBag?: BlobPropertyBag): void {
43+
const payload = blobPropertyBag ? new Blob([data], blobPropertyBag) : data;
44+
navigator.sendBeacon(url, payload);
45+
}

packages/web/src/exporters/otlp.ts

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Copyright 2024 Splunk Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { diag } from '@opentelemetry/api';
18+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
19+
import { NOOP_ATTRIBUTES_TRANSFORMER, NATIVE_XHR_SENDER, NATIVE_BEACON_SENDER, SplunkExporterConfig } from './common';
20+
import { IExportTraceServiceRequest } from '@opentelemetry/otlp-transformer';
21+
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
22+
23+
export class SplunkOTLPTraceExporter extends OTLPTraceExporter {
24+
protected readonly _onAttributesSerializing: SplunkExporterConfig['onAttributesSerializing'];
25+
protected readonly _xhrSender: SplunkExporterConfig['xhrSender'] = NATIVE_XHR_SENDER;
26+
protected readonly _beaconSender: SplunkExporterConfig['beaconSender'] = typeof navigator !== 'undefined' && navigator.sendBeacon ? NATIVE_BEACON_SENDER : undefined;
27+
28+
constructor(options: SplunkExporterConfig) {
29+
super(options);
30+
this._onAttributesSerializing = options.onAttributesSerializing || NOOP_ATTRIBUTES_TRANSFORMER;
31+
}
32+
33+
convert(spans: ReadableSpan[]): IExportTraceServiceRequest {
34+
// Changes: Add attribute serializing hook to remove data before export
35+
spans = spans.map(span => {
36+
// @ts-expect-error Yep we're overwriting a readonly property here. Deal with it
37+
span.attributes = this._onAttributesSerializing ? this._onAttributesSerializing(span.attributes, span) : span.attributes;
38+
return span;
39+
});
40+
41+
return super.convert(spans);
42+
}
43+
44+
send(
45+
items: ReadableSpan[],
46+
onSuccess: () => void,
47+
): void {
48+
if (this._shutdownOnce.isCalled) {
49+
diag.debug('Shutdown already started. Cannot send objects');
50+
return;
51+
}
52+
const serviceRequest = this.convert(items);
53+
const body = JSON.stringify(serviceRequest);
54+
55+
// Changed: Determine which exporter to use at the time of export
56+
if (document.hidden && this._beaconSender && body.length <= 64000) {
57+
this._beaconSender(this.url, body, { type: 'application/json' });
58+
} else {
59+
this._xhrSender!(this.url, body, {
60+
// These headers may only be necessary for otel's collector,
61+
// need to test with actual ingest
62+
Accept: 'application/json',
63+
'Content-Type': 'application/json',
64+
...this.headers
65+
});
66+
}
67+
68+
onSuccess();
69+
}
70+
}

0 commit comments

Comments
 (0)