diff --git a/change/@splunk-otel-2d6ee85d-fc60-475e-803d-c860c932e1f4.json b/change/@splunk-otel-2d6ee85d-fc60-475e-803d-c860c932e1f4.json new file mode 100644 index 00000000..fa96ab5e --- /dev/null +++ b/change/@splunk-otel-2d6ee85d-fc60-475e-803d-c860c932e1f4.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "add support for SPLUNK_REALM", + "packageName": "@splunk/otel", + "email": "siimkallas@gmail.com", + "dependentChangeType": "patch" +} diff --git a/docs/advanced-config.md b/docs/advanced-config.md index 495a742c..65d46e29 100644 --- a/docs/advanced-config.md +++ b/docs/advanced-config.md @@ -38,6 +38,7 @@ This distribution supports all the configuration options supported by the compon | `OTEL_TRACES_SAMPLER` | `parentbased_always_on` | Stable | Sampler to be used for traces. See [Sampling](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling) | `OTEL_TRACES_SAMPLER_ARG` | | Stable | String value to be used as the sampler argument. Only be used if OTEL_TRACES_SAMPLER is set. | `SPLUNK_ACCESS_TOKEN`
`accessToken` | | Stable | The optional access token for exporting signal data directly to SignalFx API. +| `SPLUNK_REALM`
`realm` | | Stable | The name of your organization's realm, for example, ``us0``. When you set the realm, telemetry is sent directly to the ingest endpoint of Splunk Observability Cloud, bypassing the Splunk OpenTelemetry Collector. | `SPLUNK_TRACE_RESPONSE_HEADER_ENABLED`
`serverTimingEnabled` | `true` | Stable | Enable injection of `Server-Timing` header to HTTP responses. | `SPLUNK_REDIS_INCLUDE_COMMAND_ARGS` | `false` | Stable | Will include the full redis query in `db.statement` span attribute when using `redis` instrumentation. diff --git a/src/metrics/index.ts b/src/metrics/index.ts index 0343b9c4..53209d1b 100644 --- a/src/metrics/index.ts +++ b/src/metrics/index.ts @@ -28,6 +28,7 @@ import * as signalfx from 'signalfx'; export interface MetricsOptions { accessToken: string; + realm?: string; endpoint: string; serviceName: string; // Metrics-specific configuration options: @@ -83,6 +84,7 @@ export type StartMetricsOptions = Partial & { export const allowedMetricsOptions = [ 'accessToken', + 'realm', 'endpoint', 'exportInterval', 'serviceName', @@ -284,10 +286,26 @@ export function _setDefaultOptions( ): MetricsOptions & { sfxClient: signalfx.SignalClient } { const accessToken = options.accessToken || process.env.SPLUNK_ACCESS_TOKEN || ''; - const endpoint = - options.endpoint || - process.env.SPLUNK_METRICS_ENDPOINT || - 'http://localhost:9943'; + + let endpoint = options.endpoint || process.env.SPLUNK_METRICS_ENDPOINT; + + const realm = options.realm || process.env.SPLUNK_REALM || ''; + + if (realm) { + if (!accessToken) { + throw new Error( + 'Splunk realm is set, but access token is unset. To send metrics to the Observability Cloud, both need to be set' + ); + } + + if (!endpoint) { + endpoint = `https://ingest.${realm}.signalfx.com`; + } + } + + if (!endpoint) { + endpoint = 'http://localhost:9943'; + } const resource = detectResource(); diff --git a/src/profiling/proto/profile.js b/src/profiling/proto/profile.js index e8df60ac..b96cbdbb 100644 --- a/src/profiling/proto/profile.js +++ b/src/profiling/proto/profile.js @@ -1,3 +1,4 @@ +/* istanbul ignore file */ /*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars*/ "use strict"; diff --git a/src/tracing/options.ts b/src/tracing/options.ts index 4e4309e4..074c69a6 100644 --- a/src/tracing/options.ts +++ b/src/tracing/options.ts @@ -56,6 +56,7 @@ export type CaptureHttpUriParameters = ( export interface Options { accessToken: string; + realm?: string; endpoint?: string; serviceName: string; // Tracing-specific configuration options: @@ -70,6 +71,7 @@ export interface Options { export const allowedTracingOptions = [ 'accessToken', + 'realm', 'captureHttpRequestUriParams', 'endpoint', 'instrumentations', @@ -90,6 +92,26 @@ export function _setDefaultOptions(options: Partial = {}): Options { options.accessToken = options.accessToken || process.env.SPLUNK_ACCESS_TOKEN || ''; + options.realm = options.realm || process.env.SPLUNK_REALM || ''; + + const exporterType = resolveExporterType(options); + + if (options.realm) { + if (!options.accessToken) { + throw new Error( + 'Splunk realm is set, but access token is unset. To send traces to the Observability Cloud, both need to be set' + ); + } + + if (!options.endpoint) { + if (isJaegerExporter(exporterType)) { + if (!process.env.OTEL_EXPORTER_JAEGER_ENDPOINT) { + options.endpoint = `https://ingest.${options.realm}.signalfx.com/v2/trace/jaegerthrift`; + } + } + } + } + if (options.serverTimingEnabled === undefined) { options.serverTimingEnabled = getEnvBoolean( 'SPLUNK_TRACE_RESPONSE_HEADER_ENABLED', @@ -128,7 +150,7 @@ export function _setDefaultOptions(options: Partial = {}): Options { // factories if (options.spanExporterFactory === undefined) { - options.spanExporterFactory = resolveTracesExporter(); + options.spanExporterFactory = resolveTracesExporter(exporterType); } options.spanProcessorFactory = options.spanProcessorFactory || defaultSpanProcessorFactory; @@ -167,34 +189,7 @@ export function _setDefaultOptions(options: Partial = {}): Options { }; } -export function resolveTracesExporter(): SpanExporterFactory { - const factory = - SpanExporterMap[process.env.OTEL_TRACES_EXPORTER || 'default']; - assert.strictEqual( - typeof factory, - 'function', - `Invalid value for OTEL_TRACES_EXPORTER env variable: ${util.inspect( - process.env.OTEL_TRACES_EXPORTER - )}. Pick one of ${util.inspect(Object.keys(SpanExporterMap), { - compact: true, - })} or leave undefined.` - ); - return factory; -} - -export function otlpSpanExporterFactory(options: Options): SpanExporter { - const metadata = new Metadata(); - if (options.accessToken) { - // for forward compatibility, is not currently supported - metadata.set('X-SF-TOKEN', options.accessToken); - } - return new OTLPTraceExporter({ - url: options.endpoint, - metadata, - }); -} - -function genericJaegerSpanExporterFactory( +function jaegerThriftSpanExporterFactory( defaultEndpoint: string, options: Options ): SpanExporter { @@ -217,20 +212,27 @@ function genericJaegerSpanExporterFactory( return new JaegerExporter(jaegerOptions); } -export const jaegerSpanExporterFactory = genericJaegerSpanExporterFactory.bind( +export const jaegerSpanExporterFactory = jaegerThriftSpanExporterFactory.bind( null, 'http://localhost:14268/v1/traces' ); -export const splunkSpanExporterFactory = genericJaegerSpanExporterFactory.bind( +export const splunkSpanExporterFactory = jaegerThriftSpanExporterFactory.bind( null, 'http://localhost:9080/v1/trace' ); -export function consoleSpanExporterFactory(): SpanExporter { - return new ConsoleSpanExporter(); -} +const SUPPORTED_EXPORTER_TYPES = [ + 'default', + 'console-splunk', + 'jaeger-thrift-http', + 'jaeger-thrift-splunk', + 'otlp', + 'otlp-grpc', +]; -const SpanExporterMap: Record = { +type ExporterType = typeof SUPPORTED_EXPORTER_TYPES[number]; + +const SpanExporterMap: Record = { default: otlpSpanExporterFactory, 'console-splunk': consoleSpanExporterFactory, 'jaeger-thrift-http': jaegerSpanExporterFactory, @@ -239,6 +241,72 @@ const SpanExporterMap: Record = { 'otlp-grpc': otlpSpanExporterFactory, }; +function isSupportedRealmExporter(exporterType: string) { + return ['jaeger-thrift-splunk', 'jaeger-thrift-http'].includes(exporterType); +} + +function isValidExporterType(type: string): boolean { + return SUPPORTED_EXPORTER_TYPES.includes(type); +} + +function isJaegerExporter(exporterType: ExporterType): boolean { + return ['jaeger-thrift-splunk', 'jaeger-thrift-http'].includes(exporterType); +} + +function resolveExporterType(options: Partial): ExporterType { + let tracesExporter: string | undefined = process.env.OTEL_TRACES_EXPORTER; + + if (options.realm) { + if (tracesExporter) { + if (!isSupportedRealmExporter(tracesExporter)) { + throw new Error( + 'Setting the Splunk realm with an explicit OTEL_TRACES_EXPORTER requires OTEL_TRACES_EXPORTER to be jaeger-thrift-splunk' + ); + } + } else { + tracesExporter = 'jaeger-thrift-splunk'; + } + } + + if (tracesExporter === undefined) { + tracesExporter = 'default'; + } + + if (!isValidExporterType(tracesExporter)) { + throw new Error( + `Invalid value for OTEL_TRACES_EXPORTER env variable: ${util.inspect( + process.env.OTEL_TRACES_EXPORTER + )}. Pick one of ${util.inspect(SUPPORTED_EXPORTER_TYPES, { + compact: true, + })} or leave undefined.` + ); + } + + return tracesExporter; +} + +export function resolveTracesExporter( + exporterType: ExporterType +): SpanExporterFactory { + return SpanExporterMap[exporterType]; +} + +export function otlpSpanExporterFactory(options: Options): SpanExporter { + const metadata = new Metadata(); + if (options.accessToken) { + // for forward compatibility, is not currently supported + metadata.set('X-SF-TOKEN', options.accessToken); + } + return new OTLPTraceExporter({ + url: options.endpoint, + metadata, + }); +} + +export function consoleSpanExporterFactory(): SpanExporter { + return new ConsoleSpanExporter(); +} + // Temporary workaround until https://github.com/open-telemetry/opentelemetry-js/issues/3094 is resolved function getBatchSpanProcessorConfig() { // OTel uses its own parsed environment, we can just use the default env if the BSP delay is unset. diff --git a/test/metrics.test.ts b/test/metrics.test.ts index c85dd996..ba16cb50 100644 --- a/test/metrics.test.ts +++ b/test/metrics.test.ts @@ -126,6 +126,32 @@ describe('metrics', () => { node_version: process.versions.node, }); }); + + it('throws when realm is set without an access token', () => { + process.env.SPLUNK_REALM = 'eu0'; + assert.throws( + _setDefaultOptions, + /To send metrics to the Observability Cloud/ + ); + }); + + it('chooses the correct endpoint when realm is set', () => { + process.env.SPLUNK_REALM = 'eu0'; + process.env.SPLUNK_ACCESS_TOKEN = 'abc'; + const options = _setDefaultOptions(); + assert.deepStrictEqual( + options.endpoint, + 'https://ingest.eu0.signalfx.com' + ); + }); + + it('prefers user endpoint when realm is set', () => { + process.env.SPLUNK_REALM = 'eu0'; + process.env.SPLUNK_ACCESS_TOKEN = 'abc'; + process.env.SPLUNK_METRICS_ENDPOINT = 'http://localhost:9999'; + const options = _setDefaultOptions(); + assert.deepStrictEqual(options.endpoint, 'http://localhost:9999'); + }); }); describe('startMetrics', () => { diff --git a/test/options.test.ts b/test/options.test.ts index e4282f5e..92e97ba1 100644 --- a/test/options.test.ts +++ b/test/options.test.ts @@ -33,6 +33,7 @@ import * as instrumentations from '../src/instrumentations'; import { _setDefaultOptions, defaultPropagatorFactory, + jaegerSpanExporterFactory, otlpSpanExporterFactory, splunkSpanExporterFactory, defaultSpanProcessorFactory, @@ -218,6 +219,41 @@ describe('options', () => { 'foobar' ); }); + + describe('Splunk Realm', () => { + beforeEach(utils.cleanEnvironment); + + it('throws when setting SPLUNK_REALM without an access token', () => { + process.env.SPLUNK_REALM = 'us0'; + assert.throws( + _setDefaultOptions, + /Splunk realm is set, but access token is unset/ + ); + }); + + it('chooses the correct Jaeger Thrift endpoint when realm is set', () => { + process.env.SPLUNK_REALM = 'us0'; + process.env.SPLUNK_ACCESS_TOKEN = 'abc'; + + const options = _setDefaultOptions(); + assert.deepStrictEqual( + options.endpoint, + 'https://ingest.us0.signalfx.com/v2/trace/jaegerthrift' + ); + assert.deepStrictEqual( + options.spanExporterFactory, + splunkSpanExporterFactory + ); + }); + + it('throws when setting the realm with an incompatible SPLUNK_TRACES_EXPORTER', () => { + process.env.SPLUNK_REALM = 'us0'; + process.env.SPLUNK_ACCESS_TOKEN = 'abc'; + process.env.OTEL_TRACES_EXPORTER = 'otlp-grpc'; + + assert.throws(_setDefaultOptions, /jaeger-thrift-splunk/); + }); + }); }); class TestInstrumentation extends InstrumentationBase {