Skip to content

Commit

Permalink
feat: OTEL logging support (#876)
Browse files Browse the repository at this point in the history
* Add logging

* Add back logging deps

* Change log msg
  • Loading branch information
mhennoch authored Feb 12, 2024
1 parent 56829ad commit 8725aaa
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 8 deletions.
33 changes: 33 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,10 @@
"@grpc/grpc-js": "^1.9.14",
"@grpc/proto-loader": "^0.7.10",
"@opentelemetry/api": "^1.3.0",
"@opentelemetry/api-logs": "^0.48.0",
"@opentelemetry/context-async-hooks": "1.21.0",
"@opentelemetry/core": "1.21.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.48.0",
"@opentelemetry/exporter-metrics-otlp-grpc": "0.48.0",
"@opentelemetry/exporter-metrics-otlp-proto": "0.48.0",
"@opentelemetry/exporter-trace-otlp-grpc": "0.48.0",
Expand Down Expand Up @@ -145,6 +147,7 @@
"@opentelemetry/instrumentation-winston": "0.34.0",
"@opentelemetry/propagator-b3": "1.21.0",
"@opentelemetry/resources": "1.21.0",
"@opentelemetry/sdk-logs": "^0.48.0",
"@opentelemetry/sdk-metrics": "1.21.0",
"@opentelemetry/sdk-trace-base": "1.21.0",
"@opentelemetry/sdk-trace-node": "1.21.0",
Expand Down
5 changes: 5 additions & 0 deletions src/instrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import { startMetrics } from './metrics';
import { startProfiling } from './profiling';
import { startTracing } from './tracing';
import { startLogging } from './logging';

function boot() {
const logLevel = parseLogLevel(getNonEmptyEnvVar('OTEL_LOG_LEVEL'));
Expand All @@ -51,6 +52,10 @@ function boot() {

startTracing();

if (getEnvBoolean('SPLUNK_AUTOMATIC_LOG_COLLECTION', false)) {
startLogging();
}

if (
getEnvBoolean('SPLUNK_METRICS_ENABLED', false) ||
getEnvBoolean('SPLUNK_PROFILER_MEMORY_ENABLED', false)
Expand Down
161 changes: 161 additions & 0 deletions src/logging/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as util from 'util';
import * as logsAPI from '@opentelemetry/api-logs';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { diag } from '@opentelemetry/api';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import {
LoggerProvider,
BatchLogRecordProcessor,
LogRecordProcessor,
ConsoleLogRecordExporter,
} from '@opentelemetry/sdk-logs';

import { getNonEmptyEnvVar, getEnvArray, defaultServiceName } from '../utils';
import { detect as detectResource } from '../resource';

type LogRecordProcessorFactory = (
options: LoggingOptions
) => LogRecordProcessor | LogRecordProcessor[];

interface LoggingOptions {
accessToken?: string;
realm?: string;
serviceName: string;
endpoint?: string;
resource: Resource;
logRecordProcessorFactory: LogRecordProcessorFactory;
}

export const allowedLoggingOptions = [
'accessToken',
'realm',
'serviceName',
'endpoint',
'logRecordProcessorFactory',
];

export type StartLoggingOptions = Partial<Omit<LoggingOptions, 'resource'>>;

export function startLogging(opts: StartLoggingOptions = {}) {
const options = _setDefaultOptions(opts);
const loggerProvider = new LoggerProvider({
resource: options.resource,
});

let processors = options.logRecordProcessorFactory(options);

if (!Array.isArray(processors)) {
processors = [processors];
}

processors.forEach((processor) =>
loggerProvider.addLogRecordProcessor(processor)
);

logsAPI.logs.setGlobalLoggerProvider(loggerProvider);

return {
stop: () => {
return loggerProvider.shutdown();
},
};
}

export function _setDefaultOptions(
options: StartLoggingOptions = {}
): LoggingOptions {
let resource = detectResource();

const serviceName =
options.serviceName ||
getNonEmptyEnvVar('OTEL_SERVICE_NAME') ||
resource.attributes[SemanticResourceAttributes.SERVICE_NAME];

if (!serviceName) {
diag.warn(
'service.name attribute for logging is not set, your service is unnamed and will be difficult to identify. ' +
'Set your service name using the OTEL_RESOURCE_ATTRIBUTES environment variable. ' +
'E.g. OTEL_RESOURCE_ATTRIBUTES="service.name=<YOUR_SERVICE_NAME_HERE>"'
);
}

resource = resource.merge(
new Resource({
[SemanticResourceAttributes.SERVICE_NAME]:
serviceName || defaultServiceName(),
})
);

options.logRecordProcessorFactory =
options.logRecordProcessorFactory || defaultlogRecordProcessorFactory;

return {
serviceName: String(
resource.attributes[SemanticResourceAttributes.SERVICE_NAME]
),
endpoint: options.endpoint, // will use default collector url if not set
logRecordProcessorFactory: options.logRecordProcessorFactory,
resource,
};
}

const SUPPORTED_EXPORTER_TYPES = ['console', 'otlp'];

function areValidExporterTypes(types: string[]): boolean {
return types.every((t) => SUPPORTED_EXPORTER_TYPES.includes(t));
}

function createExporters(options: LoggingOptions) {
const logExporters: string[] = getEnvArray('OTEL_LOGS_EXPORTER', ['otlp']);

if (!areValidExporterTypes(logExporters)) {
throw new Error(
`Invalid value for OTEL_LOGS_EXPORTER env variable: ${util.inspect(
getNonEmptyEnvVar('OTEL_LOGS_EXPORTER')
)}. Choose from ${util.inspect(SUPPORTED_EXPORTER_TYPES, {
compact: true,
})} or leave undefined.`
);
}

return logExporters.flatMap((type) => {
switch (type) {
case 'otlp':
return new OTLPLogExporter({
url: options.endpoint,
});
case 'console':
return new ConsoleLogRecordExporter();
default:
return [];
}
});
}

export function defaultlogRecordProcessorFactory(
options: LoggingOptions
): LogRecordProcessor[] {
let exporters = createExporters(options);

if (!Array.isArray(exporters)) {
exporters = [exporters];
}
return exporters.map((exporter) => new BatchLogRecordProcessor(exporter, {}));
}
30 changes: 28 additions & 2 deletions src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ import {
MeterOptions,
createNoopMeter,
} from '@opentelemetry/api';
import {
StartLoggingOptions,
allowedLoggingOptions,
startLogging,
} from './logging';

interface Options {
accessToken: string;
Expand All @@ -56,18 +61,21 @@ interface Options {
metrics: boolean | StartMetricsOptions;
profiling: boolean | StartProfilingOptions;
tracing: boolean | StartTracingOptions;
logging: boolean | StartLoggingOptions;
}

interface RunningState {
metrics: ReturnType<typeof startMetrics> | null;
profiling: ReturnType<typeof startProfiling> | null;
tracing: ReturnType<typeof startTracing> | null;
logging: ReturnType<typeof startLogging> | null;
}

const running: RunningState = {
metrics: null,
profiling: null,
tracing: null,
logging: null,
};

function isSignalEnabled<T>(
Expand All @@ -79,10 +87,15 @@ function isSignalEnabled<T>(
}

export const start = (options: Partial<Options> = {}) => {
if (running.metrics || running.profiling || running.tracing) {
if (
running.logging ||
running.metrics ||
running.profiling ||
running.tracing
) {
throw new Error('Splunk APM already started');
}
const { metrics, profiling, tracing, ...restOptions } = options;
const { metrics, profiling, tracing, logging, ...restOptions } = options;

assertNoExtraneousProperties(restOptions, [
'accessToken',
Expand Down Expand Up @@ -122,6 +135,14 @@ export const start = (options: Partial<Options> = {}) => {
);
}

if (
isSignalEnabled(options.logging, 'SPLUNK_AUTOMATIC_LOG_COLLECTION', false)
) {
running.logging = startLogging(
Object.assign(pick(restOptions, allowedLoggingOptions), logging)
);
}

if (
isSignalEnabled(
options.metrics,
Expand Down Expand Up @@ -162,6 +183,11 @@ function createNoopMeterProvider() {
export const stop = async () => {
const promises = [];

if (running.logging) {
promises.push(running.logging.stop());
running.logging = null;
}

if (running.metrics) {
promises.push(running.metrics.stop());
running.metrics = null;
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type EnvVarKey =
| 'OTEL_EXPORTER_OTLP_TRACES_PROTOCOL'
| 'OTEL_INSTRUMENTATION_COMMON_DEFAULT_ENABLED'
| 'OTEL_LOG_LEVEL'
| 'OTEL_LOGS_EXPORTER'
| 'OTEL_METRIC_EXPORT_INTERVAL'
| 'OTEL_METRICS_EXPORTER'
| 'OTEL_PROPAGATORS'
Expand Down
45 changes: 45 additions & 0 deletions test/logging.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as assert from 'assert';
import { startLogging, _setDefaultOptions } from '../src/logging';
import * as logsAPI from '@opentelemetry/api-logs';
import {
LoggerProvider,
SimpleLogRecordProcessor,
ConsoleLogRecordExporter,
} from '@opentelemetry/sdk-logs';

describe('logging', () => {
describe('startLogging', () => {
it('sets logprovider', () => {
startLogging();
const provider = logsAPI.logs.getLoggerProvider();
assert(provider instanceof LoggerProvider);
});

it('allows overriding log processors', () => {
const options = _setDefaultOptions({
logRecordProcessorFactory: (options) => {
return new SimpleLogRecordProcessor(new ConsoleLogRecordExporter());
},
serviceName: '',
});
const exporter = options.logRecordProcessorFactory(options);
assert(exporter instanceof SimpleLogRecordProcessor);
});
});
});
Loading

0 comments on commit 8725aaa

Please sign in to comment.