Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support SPLUNK_REALM #530

Merged
merged 7 commits into from
Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "add support for SPLUNK_REALM",
"packageName": "@splunk/otel",
"email": "[email protected]",
"dependentChangeType": "patch"
}
1 change: 1 addition & 0 deletions docs/advanced-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`<br>`accessToken` | | Stable | The optional access token for exporting signal data directly to SignalFx API.
| `SPLUNK_REALM`<br>`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`<br>`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.

Expand Down
26 changes: 22 additions & 4 deletions src/metrics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import * as signalfx from 'signalfx';

export interface MetricsOptions {
accessToken: string;
realm?: string;
endpoint: string;
serviceName: string;
// Metrics-specific configuration options:
Expand Down Expand Up @@ -83,6 +84,7 @@ export type StartMetricsOptions = Partial<MetricsOptions> & {

export const allowedMetricsOptions = [
'accessToken',
'realm',
'endpoint',
'exportInterval',
'serviceName',
Expand Down Expand Up @@ -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();

Expand Down
1 change: 1 addition & 0 deletions src/profiling/proto/profile.js
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
138 changes: 103 additions & 35 deletions src/tracing/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export type CaptureHttpUriParameters = (

export interface Options {
accessToken: string;
realm?: string;
endpoint?: string;
serviceName: string;
// Tracing-specific configuration options:
Expand All @@ -70,6 +71,7 @@ export interface Options {

export const allowedTracingOptions = [
'accessToken',
'realm',
'captureHttpRequestUriParams',
'endpoint',
'instrumentations',
Expand All @@ -90,6 +92,26 @@ export function _setDefaultOptions(options: Partial<Options> = {}): 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',
Expand Down Expand Up @@ -128,7 +150,7 @@ export function _setDefaultOptions(options: Partial<Options> = {}): Options {

// factories
if (options.spanExporterFactory === undefined) {
options.spanExporterFactory = resolveTracesExporter();
options.spanExporterFactory = resolveTracesExporter(exporterType);
}
options.spanProcessorFactory =
options.spanProcessorFactory || defaultSpanProcessorFactory;
Expand Down Expand Up @@ -167,34 +189,7 @@ export function _setDefaultOptions(options: Partial<Options> = {}): 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 {
Expand All @@ -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<string, SpanExporterFactory> = {
type ExporterType = typeof SUPPORTED_EXPORTER_TYPES[number];

const SpanExporterMap: Record<ExporterType, SpanExporterFactory> = {
default: otlpSpanExporterFactory,
'console-splunk': consoleSpanExporterFactory,
'jaeger-thrift-http': jaegerSpanExporterFactory,
Expand All @@ -239,6 +241,72 @@ const SpanExporterMap: Record<string, SpanExporterFactory> = {
'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<Options>): 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.
Expand Down
26 changes: 26 additions & 0 deletions test/metrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
36 changes: 36 additions & 0 deletions test/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import * as instrumentations from '../src/instrumentations';
import {
_setDefaultOptions,
defaultPropagatorFactory,
jaegerSpanExporterFactory,
otlpSpanExporterFactory,
splunkSpanExporterFactory,
defaultSpanProcessorFactory,
Expand Down Expand Up @@ -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 {
Expand Down