feat(diag-logger): introduce a new global level api.diag for internal diagnostic logging (#1880)

Co-authored-by: Daniel Dyla <dyladan@users.noreply.github.com>
This commit is contained in:
Nev 2021-02-10 14:12:26 -08:00 committed by GitHub
parent 000a8ac099
commit 1d682c2f75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1287 additions and 17 deletions

3
.gitignore vendored
View File

@ -78,6 +78,9 @@ package.json.lerna_backup
# VsCode configs
.vscode/
#Visual Studio
.vs/
#IDEA
.idea
*.iml

View File

@ -4,7 +4,7 @@ const benchmark = require('./benchmark');
const opentelemetry = require('../packages/opentelemetry-api');
const { BasicTracerProvider, BatchSpanProcessor, InMemorySpanExporter, SimpleSpanProcessor } = require('../packages/opentelemetry-tracing');
const logger = new opentelemetry.NoopLogger();
const diagLogger = opentelemetry.createNoopDiagLogger();
const setups = [
{
@ -13,7 +13,7 @@ const setups = [
},
{
name: 'BasicTracerProvider',
provider: new BasicTracerProvider({ logger })
provider: new BasicTracerProvider({ logger: diagLogger })
},
{
name: 'BasicTracerProvider with SimpleSpanProcessor',
@ -63,7 +63,7 @@ for (const setup of setups) {
suite.run({ async: false });
}
function getProvider(processor) {
const provider = new BasicTracerProvider({ logger });
const provider = new BasicTracerProvider({ logger: diagLogger });
provider.addSpanProcessor(processor);
return provider;
}

View File

@ -1,15 +1,18 @@
'use strict';
const { ConsoleLogger, LogLevel } = require('@opentelemetry/core');
const { DiagConsoleLogger, DiagLogLevel, diag } = require('@opentelemetry/api');
const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector');
// const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector-grpc');
// const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector-proto');
const { MeterProvider } = require('@opentelemetry/metrics');
diag.setLogger(new DiagConsoleLogger());
diag.setLogLevel(DiagLogLevel.DEBUG);
const metricExporter = new CollectorMetricExporter({
serviceName: 'basic-metric-service',
// url: 'http://localhost:55681/v1/metrics',
logger: new ConsoleLogger(LogLevel.DEBUG),
logger: diag,
});
const meter = new MeterProvider({

View File

@ -1,15 +1,16 @@
'use strict';
const opentelemetry = require('@opentelemetry/api');
// const { ConsoleLogger, LogLevel} = require('@opentelemetry/core');
const { BasicTracerProvider, ConsoleSpanExporter, SimpleSpanProcessor } = require('@opentelemetry/tracing');
const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector');
// const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector-grpc');
// const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector-proto');
// opentelemetry.diag.setLogger(new opentelemetry.DiagConsoleLogger());
// opentelemetry.diag.setLogLevel(opentelemetry.DiagLogLevel.DEBUG);
const exporter = new CollectorTraceExporter({
serviceName: 'basic-service',
// logger: new ConsoleLogger(LogLevel.DEBUG),
// headers: {
// foo: 'bar'
// },

View File

@ -1,7 +1,7 @@
'use strict';
const { MeterProvider } = require('@opentelemetry/metrics');
const { ConsoleLogger, LogLevel } = require('@opentelemetry/core');
const { DiagConsoleLogger, DiagLogLevel, diagLogLevelFilter } = require('@opentelemetry/api');
const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus');
const exporter = new PrometheusExporter(
@ -61,7 +61,7 @@ meter.createBatchObserver((observerBatchResult) => {
});
}, {
maxTimeoutUpdateMS: 500,
logger: new ConsoleLogger(LogLevel.DEBUG)
logger: diagLogLevelFilter(DiagLogLevel.DEBUG, new DiagConsoleLogger())
},
);

View File

@ -1,12 +1,12 @@
'use strict';
const { ConsoleLogger, LogLevel } = require('@opentelemetry/core');
const { DiagConsoleLogger, DiagLogLevel, diagLogLevelFilter } = require('@opentelemetry/api');
const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector');
const { MeterProvider } = require('@opentelemetry/metrics');
const metricExporter = new CollectorMetricExporter({
serviceName: 'basic-metric-service',
logger: new ConsoleLogger(LogLevel.DEBUG),
logger: diagLogLevelFilter(DiagLogLevel.DEBUG, new DiagConsoleLogger()),
});
let interval;

View File

@ -0,0 +1,153 @@
/*
* Copyright The OpenTelemetry Authors
*
* 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
*
* https://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 {
DiagLogger,
DiagLogFunction,
createNoopDiagLogger,
diagLoggerFunctions,
} from '../diag/logger';
import { DiagLogLevel, createLogLevelDiagLogger } from '../diag/logLevel';
import {
API_BACKWARDS_COMPATIBILITY_VERSION,
GLOBAL_DIAG_LOGGER_API_KEY,
makeGetter,
_global,
} from './global-utils';
/** Internal simple Noop Diag API that returns a noop logger and does not allow any changes */
function noopDiagApi(): DiagAPI {
const noopApi = createNoopDiagLogger() as DiagAPI;
noopApi.getLogger = () => noopApi;
noopApi.setLogger = noopApi.getLogger;
noopApi.setLogLevel = () => {};
return noopApi;
}
/**
* Singleton object which represents the entry point to the OpenTelemetry internal
* diagnostic API
*/
export class DiagAPI implements DiagLogger {
/** Get the singleton instance of the DiagAPI API */
public static instance(): DiagAPI {
let theInst = null;
if (_global[GLOBAL_DIAG_LOGGER_API_KEY]) {
// Looks like a previous instance was set, so try and fetch it
theInst = _global[GLOBAL_DIAG_LOGGER_API_KEY]?.(
API_BACKWARDS_COMPATIBILITY_VERSION
) as DiagAPI;
}
if (!theInst) {
theInst = new DiagAPI();
_global[GLOBAL_DIAG_LOGGER_API_KEY] = makeGetter(
API_BACKWARDS_COMPATIBILITY_VERSION,
theInst,
noopDiagApi()
);
}
return theInst;
}
/**
* Private internal constructor
* @private
*/
private constructor() {
let _logLevel: DiagLogLevel = DiagLogLevel.INFO;
let _filteredLogger: DiagLogger | null;
let _logger: DiagLogger = createNoopDiagLogger();
function _logProxy(funcName: keyof DiagLogger): DiagLogFunction {
return function () {
const orgArguments = arguments as unknown;
const theLogger = _filteredLogger || _logger;
const theFunc = theLogger[funcName];
if (typeof theFunc === 'function') {
return theFunc.apply(
theLogger,
orgArguments as Parameters<DiagLogFunction>
);
}
};
}
// Using self local variable for minification purposes as 'this' cannot be minified
const self = this;
// DiagAPI specific functions
self.getLogger = (): DiagLogger => {
// Return itself if no existing logger is defined (defaults effectively to a Noop)
return _logger;
};
self.setLogger = (logger: DiagLogger): DiagLogger => {
const prevLogger = _logger;
if (prevLogger !== logger && logger !== self) {
// Simple special case to avoid any possible infinite recursion on the logging functions
_logger = logger || createNoopDiagLogger();
_filteredLogger = createLogLevelDiagLogger(_logLevel, _logger);
}
return prevLogger;
};
self.setLogLevel = (maxLogLevel: DiagLogLevel) => {
if (maxLogLevel !== _logLevel) {
_logLevel = maxLogLevel;
if (_logger) {
_filteredLogger = createLogLevelDiagLogger(maxLogLevel, _logger);
}
}
};
for (let i = 0; i < diagLoggerFunctions.length; i++) {
const name = diagLoggerFunctions[i];
self[name] = _logProxy(name);
}
}
/**
* Return the currently configured logger instance, if no logger has been configured
* it will return itself so any log level filtering will still be applied in this case.
*/
public getLogger!: () => DiagLogger;
/**
* Set the DiagLogger instance
* @param logger - The DiagLogger instance to set as the default logger
* @returns The previously registered DiagLogger
*/
public setLogger!: (logger: DiagLogger) => DiagLogger;
/** Set the default maximum diagnostic logging level */
public setLogLevel!: (maxLogLevel: DiagLogLevel) => void;
// DiagLogger implementation
public verbose!: DiagLogFunction;
public debug!: DiagLogFunction;
public info!: DiagLogFunction;
public warn!: DiagLogFunction;
public startupInfo!: DiagLogFunction;
public error!: DiagLogFunction;
public critical!: DiagLogFunction;
public terminal!: DiagLogFunction;
}

View File

@ -18,6 +18,7 @@ import { ContextManager } from '@opentelemetry/context-base';
import { TextMapPropagator } from '../context/propagation/TextMapPropagator';
import { TracerProvider } from '../trace/tracer_provider';
import { _globalThis } from '../platform';
import { DiagAPI } from '../api/diag';
export const GLOBAL_CONTEXT_MANAGER_API_KEY = Symbol.for(
'io.opentelemetry.js.api.context'
@ -28,11 +29,16 @@ export const GLOBAL_PROPAGATION_API_KEY = Symbol.for(
);
export const GLOBAL_TRACE_API_KEY = Symbol.for('io.opentelemetry.js.api.trace');
export const GLOBAL_DIAG_LOGGER_API_KEY = Symbol.for(
'io.opentelemetry.js.api.diag'
);
type Get<T> = (version: number) => T;
type OtelGlobal = Partial<{
[GLOBAL_CONTEXT_MANAGER_API_KEY]: Get<ContextManager>;
[GLOBAL_PROPAGATION_API_KEY]: Get<TextMapPropagator>;
[GLOBAL_TRACE_API_KEY]: Get<TracerProvider>;
[GLOBAL_DIAG_LOGGER_API_KEY]: Get<DiagAPI>;
}>;
export const _global = _globalThis as OtelGlobal;

View File

@ -16,7 +16,10 @@
export type LogFunction = (message: string, ...args: unknown[]) => void;
/** Defines a logger interface. */
/** Defines a logger interface.
* @deprecated This interface will be removed prior to v1.0, use the api.diag
* @see {@link DiagLogger} and {@link DiagAPI}
*/
export interface Logger {
error: LogFunction;
warn: LogFunction;

View File

@ -0,0 +1,112 @@
/*
* Copyright The OpenTelemetry Authors
*
* 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
*
* https://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 { DiagLogger, DiagLogFunction } from './logger';
const consoleMap: { n: keyof DiagLogger; c: keyof Console }[] = [
{ n: 'terminal', c: 'error' },
{ n: 'critical', c: 'error' },
{ n: 'error', c: 'error' },
{ n: 'warn', c: 'warn' },
{ n: 'info', c: 'info' },
{ n: 'debug', c: 'debug' },
{ n: 'verbose', c: 'trace' },
{ n: 'startupInfo', c: 'info' },
];
/**
* A simple Immutable Console based diagnostic logger which will output any messages to the Console.
* If you want to limit the amount of logging to a specific level or lower use the
* {@link diagLogLevelFilter}
*/
export class DiagConsoleLogger implements DiagLogger {
constructor() {
function _consoleFunc(funcName: keyof Console): DiagLogFunction {
return function () {
const orgArguments = arguments;
if (console) {
// Some environments only expose the console when the F12 developer console is open
let theFunc = console[funcName];
if (typeof theFunc !== 'function') {
// Not all environments support all functions
theFunc = console.log;
}
// One last final check
if (typeof theFunc === 'function') {
return theFunc.apply(console, orgArguments);
}
}
};
}
for (let i = 0; i < consoleMap.length; i++) {
this[consoleMap[i].n] = _consoleFunc(consoleMap[i].c);
}
}
/**
* Log a terminal situation that would cause the API to completely fail to initialize,
* if this type of message is logged functionality of the API is not expected to be functional.
*/
public terminal!: DiagLogFunction;
/**
* Log a critical error that NEEDS to be addressed, functionality of the component that emits
* this log detail may non-functional. While the overall API may be.
*/
public critical!: DiagLogFunction;
/** Log an error scenario that was not expected and caused the requested operation to fail. */
public error!: DiagLogFunction;
/**
* Logs a general informational message that is used for logging component startup and version
* information without causing additional general informational messages when the logging level
* is set to DiagLogLevel.WARN or lower.
*/
public startupInfo!: DiagLogFunction;
/**
* Log a warning scenario to inform the developer of an issues that should be investigated.
* The requested operation may or may not have succeeded or completed.
*/
public warn!: DiagLogFunction;
/**
* Log a general informational message, this should not affect functionality.
* This is also the default logging level so this should NOT be used for logging
* debugging level information.
*/
public info!: DiagLogFunction;
/**
* Log a general debug message that can be useful for identifying a failure.
* Information logged at this level may include diagnostic details that would
* help identify a failure scenario. Useful scenarios would be to log the execution
* order of async operations
*/
public debug!: DiagLogFunction;
/**
* Log a detailed (verbose) trace level logging that can be used to identify failures
* where debug level logging would be insufficient, this level of tracing can include
* input and output parameters and as such may include PII information passing through
* the API. As such it is recommended that this level of tracing should not be enabled
* in a production environment.
*/
public verbose!: DiagLogFunction;
}

View File

@ -0,0 +1,161 @@
/*
* Copyright The OpenTelemetry Authors
*
* 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
*
* https://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 { DiagAPI } from '../api/diag';
import { Logger } from '../common/Logger';
import { DiagLogger, DiagLogFunction, createNoopDiagLogger } from './logger';
/**
* Defines the available internal logging levels for the diagnostic logger, the numeric values
* of the levels are defined to match the original values from the initial LogLevel to avoid
* compatibility/migration issues for any implementation that assume the numeric ordering.
*/
export enum DiagLogLevel {
/** Diagnostic Logging level setting to disable all logging (except and forced logs) */
NONE = 0,
/**
* Identifies a terminal situation that would cause the API to completely fail to initialize,
* if this type of error is logged functionality of the API is not expected to be functional.
*/
TERMINAL = 10,
/**
* Identifies a critical error that needs to be addressed, functionality of the component
* that emits this log detail may non-functional.
*/
CRITICAL = 20,
/** Identifies an error scenario */
ERROR = 30,
/** Identifies startup and failure (lower) scenarios */
STARTUP = 40,
/** Identifies a warning scenario */
WARN = 50,
/** General informational log message */
INFO = 60,
/** General debug log message */
DEBUG = 70,
/**
* Detailed trace level logging should only be used for development, should only be set
* in a development environment.
*/
VERBOSE = 80,
/** Used to set the logging level to include all logging */
ALL = 9999,
}
/**
* This is equivalent to:
* type LogLevelString = 'NONE' | TERMINAL' | 'CRITICAL' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'VERBOSE' | 'ALL';
*/
export type DiagLogLevelString = keyof typeof DiagLogLevel;
/**
* Mapping from DiagLogger function name to Legacy Logger function used if
* the logger instance doesn't have the DiagLogger function
*/
const fallbackLoggerFuncMap: { [n: string]: keyof Logger } = {
terminal: 'error',
critical: 'error',
error: 'error',
warn: 'warn',
info: 'info',
debug: 'debug',
verbose: 'debug',
startupInfo: 'info',
};
/** Mapping from DiagLogger function name to logging level. */
const levelMap: { n: keyof DiagLogger; l: DiagLogLevel }[] = [
{ n: 'terminal', l: DiagLogLevel.TERMINAL },
{ n: 'critical', l: DiagLogLevel.CRITICAL },
{ n: 'error', l: DiagLogLevel.ERROR },
{ n: 'warn', l: DiagLogLevel.WARN },
{ n: 'info', l: DiagLogLevel.INFO },
{ n: 'debug', l: DiagLogLevel.DEBUG },
{ n: 'verbose', l: DiagLogLevel.VERBOSE },
{ n: 'startupInfo', l: DiagLogLevel.ERROR },
];
/**
* Create a Diagnostic logger which limits the messages that are logged via the wrapped logger based on whether the
* message has a {@link DiagLogLevel} equal to the maximum logging level or lower, unless the {@link DiagLogLevel} is
* NONE which will return a noop logger instance. This can be useful to reduce the amount of logging used for the
* system or for a specific component based on any local configuration.
* If you don't supply a logger it will use the global api.diag as the destination which will use the
* current logger and any filtering it may have applied.
* To avoid / bypass any global level filtering you should pass the current logger returned via
* api.diag.getLogger() however, any changes to the logger used by api.diag won't be reflected for this case.
* @param maxLevel - The max level to log any logging of a lower
* @param logger - The specific logger to limit, if not defined or supplied will default to api.diag
* @implements {@link DiagLogger}
* @returns {DiagLogger}
*/
export function createLogLevelDiagLogger(
maxLevel: DiagLogLevel,
logger?: DiagLogger | null
): DiagLogger {
if (maxLevel < DiagLogLevel.NONE) {
maxLevel = DiagLogLevel.NONE;
} else if (maxLevel > DiagLogLevel.ALL) {
maxLevel = DiagLogLevel.ALL;
}
if (maxLevel === DiagLogLevel.NONE) {
return createNoopDiagLogger();
}
if (!logger) {
logger = DiagAPI.instance();
}
function _filterFunc(
theLogger: DiagLogger,
funcName: keyof DiagLogger,
theLevel: DiagLogLevel
): DiagLogFunction {
if (maxLevel >= theLevel) {
return function () {
const orgArguments = arguments as unknown;
const theFunc =
theLogger[funcName] || theLogger[fallbackLoggerFuncMap[funcName]];
if (theFunc && typeof theFunc === 'function') {
return theFunc.apply(
logger,
orgArguments as Parameters<DiagLogFunction>
);
}
};
}
return function () {};
}
const newLogger = {} as DiagLogger;
for (let i = 0; i < levelMap.length; i++) {
const name = levelMap[i].n;
newLogger[name] = _filterFunc(logger, name, levelMap[i].l);
}
return newLogger;
}

View File

@ -0,0 +1,121 @@
/*
* Copyright The OpenTelemetry Authors
*
* 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
*
* https://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 { Logger } from '../common/Logger';
/**
* Defines a type which can be used for as a parameter without breaking backward
* compatibility. The {@link Logger} reference will be removed with the removal
* of the Logger definition, this can be used as a replacement for functions
* that are currently passing Logger references during migration to minimize
* breaks that will occur with the removal of the Logger interface.
*/
export type OptionalDiagLogger = Logger | DiagLogger | null | undefined;
export type DiagLogFunction = (message: string, ...args: unknown[]) => void;
/**
* Defines an internal diagnostic logger interface which is used to log internal diagnostic
* messages, you can set the default diagnostic logger via the {@link DiagAPI} setLogger function.
* API provided implementations include :-
* - a No-Op {@link createNoopDiagLogger}
* - a {@link DiagLogLevel} filtering wrapper {@link diagLogLevelFilter}
* - a general Console {@link DiagConsoleLogger} version.
*/
export interface DiagLogger {
/**
* Log a terminal situation that would cause the API to completely fail to initialize,
* if this type of message is logged functionality of the API is not expected to be functional.
*/
terminal: DiagLogFunction;
/**
* Log a critical error that NEEDS to be addressed, functionality of the component that emits
* this log detail may be limited or non-functional depending on when this message is emitted.
* Unlike terminal message, it is expected that the overall API may still be functional, again
* depending on what component and when this message is emitted.
*/
critical: DiagLogFunction;
/** Log an error scenario that was not expected and caused the requested operation to fail. */
error: DiagLogFunction;
/**
* Logs a general informational message that is used for logging component startup and version
* information without causing additional general informational messages when the logging level
* is set to DiagLogLevel.WARN or lower.
*/
startupInfo: DiagLogFunction;
/**
* Log a warning scenario to inform the developer of an issues that should be investigated.
* The requested operation may or may not have succeeded or completed.
*/
warn: DiagLogFunction;
/**
* Log a general informational message, this should not affect functionality.
* This is also the default logging level so this should NOT be used for logging
* debugging level information.
*/
info: DiagLogFunction;
/**
* Log a general debug message that can be useful for identifying a failure.
* Information logged at this level may include diagnostic details that would
* help identify a failure scenario.
* For example: Logging the order of execution of async operations.
*/
debug: DiagLogFunction;
/**
* Log a detailed (verbose) trace level logging that can be used to identify failures
* where debug level logging would be insufficient, this level of tracing can include
* input and output parameters and as such may include PII information passing through
* the API. As such it is recommended that this level of tracing should not be enabled
* in a production environment.
*/
verbose: DiagLogFunction;
}
// DiagLogger implementation
export const diagLoggerFunctions: Array<keyof DiagLogger> = [
'verbose',
'debug',
'info',
'warn',
'startupInfo',
'error',
'critical',
'terminal',
];
function noopLogFunction() {}
/**
* Returns a No-Op Diagnostic logger where all messages do nothing.
* @implements {@link DiagLogger}
* @returns {DiagLogger}
*/
export function createNoopDiagLogger(): DiagLogger {
const diagLogger = {} as DiagLogger;
for (let i = 0; i < diagLoggerFunctions.length; i++) {
diagLogger[diagLoggerFunctions[i]] = noopLogFunction;
}
return diagLogger;
}

View File

@ -42,6 +42,9 @@ export * from './trace/trace_flags';
export * from './trace/trace_state';
export * from './trace/tracer_provider';
export * from './trace/tracer';
export * from './diag/consoleLogger';
export * from './diag/logger';
export * from './diag/logLevel';
export {
INVALID_SPANID,
@ -74,8 +77,20 @@ export type { PropagationAPI } from './api/propagation';
/** Entrypoint for propagation API */
export const propagation = PropagationAPI.getInstance();
import { DiagAPI } from './api/diag';
export type { DiagAPI } from './api/diag';
/**
* Entrypoint for Diag API.
* Defines Diagnostic handler used for internal diagnostic logging operations.
* The default provides a Noop DiagLogger implementation which may be changed via the
* diag.setLogger(logger: DiagLogger) function.
*/
export const diag = DiagAPI.instance();
export default {
trace,
context,
propagation,
diag,
};

View File

@ -16,7 +16,10 @@
import { Logger } from '../common/Logger';
/** No-op implementation of Logger */
/** No-op implementation of Logger
* @deprecated This class will be removed prior to v1.0, use the api.diag
* @see {@link DiagAPI}, {@link DiagLogger} and {@link createNoopDiagLogger}
*/
export class NoopLogger implements Logger {
// By default does nothing
debug(_message: string, ..._args: unknown[]) {}

View File

@ -0,0 +1,212 @@
/*
* Copyright The OpenTelemetry Authors
*
* 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
*
* https://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 { DiagConsoleLogger } from '../../src/diag/consoleLogger';
import { diagLoggerFunctions } from '../../src/diag/logger';
const consoleFuncs: Array<keyof Console> = [
'debug',
'info',
'warn',
'error',
'log',
'trace',
];
const expectedConsoleMap: { [n: string]: keyof Console } = {
terminal: 'error',
critical: 'error',
error: 'error',
warn: 'warn',
info: 'info',
debug: 'debug',
verbose: 'trace',
startupInfo: 'info',
};
describe('DiagConsoleLogger', () => {
const origConsole = console;
const orig: any = {};
const calledArgs: any = {};
// Save original functions
consoleFuncs.forEach(fName => {
orig[fName] = console[fName];
calledArgs[fName] = null;
});
let canMockConsole = true;
try {
// eslint-disable-next-line no-global-assign
console = origConsole;
} catch (ex) {
// Not supported on CI pipeline (works locally)
canMockConsole = false;
}
beforeEach(() => {
// mock Console
consoleFuncs.forEach(fName => {
console[fName] = (...args: unknown[]) => {
calledArgs[fName] = args;
};
});
});
afterEach(() => {
// restore
if (canMockConsole) {
try {
// eslint-disable-next-line no-global-assign
console = origConsole;
} catch (ex) {
// Not supported on CI pipeline
canMockConsole = false;
}
}
consoleFuncs.forEach(fName => {
calledArgs[fName] = null;
console[fName] = orig[fName];
});
});
describe('constructor', () => {
diagLoggerFunctions.forEach(fName => {
it(`console logger should provide ${fName} function`, () => {
const consoleLogger: any = new DiagConsoleLogger();
consoleLogger[fName](`${fName} called %s`, 'param1');
assert.ok(
typeof consoleLogger[fName] === 'function',
`Must have a ${fName} function`
);
});
it(`should log ${expectedConsoleMap[fName]} message with ${fName} call only`, () => {
const consoleLogger: any = new DiagConsoleLogger();
consoleLogger[fName](`${fName} called %s`, 'param1');
// Make sure only gets logged once
let matches = 0;
consoleFuncs.forEach(cName => {
if (cName !== expectedConsoleMap[fName]) {
assert.deepStrictEqual(calledArgs[cName], null);
} else {
assert.deepStrictEqual(calledArgs[expectedConsoleMap[fName]], [
`${fName} called %s`,
'param1',
]);
matches++;
}
});
assert.deepStrictEqual(calledArgs.log, null);
assert.strictEqual(matches, 1, 'should log at least once');
});
consoleFuncs.forEach(cName => {
it(`should log ${fName} message even when console doesn't support ${cName} call before construction`, () => {
console[cName] = undefined;
const consoleLogger: any = new DiagConsoleLogger();
consoleLogger[fName](`${fName} called %s`, 'param1');
if (cName !== expectedConsoleMap[fName]) {
assert.deepStrictEqual(calledArgs[cName], null);
} else {
assert.deepStrictEqual(calledArgs.log, [
`${fName} called %s`,
'param1',
]);
}
});
it(`should log ${fName} message even when console doesn't support ${cName} call after construction`, () => {
const consoleLogger: any = new DiagConsoleLogger();
console[cName] = undefined;
consoleLogger[fName](`${fName} called %s`, 'param1');
if (cName !== expectedConsoleMap[fName]) {
assert.deepStrictEqual(calledArgs[cName], null);
} else {
assert.deepStrictEqual(calledArgs.log, [
`${fName} called %s`,
'param1',
]);
}
});
});
});
if (canMockConsole) {
diagLoggerFunctions.forEach(fName => {
const cName = expectedConsoleMap[fName];
it(`should not throw even when console is not supported for ${fName} call`, () => {
// eslint-disable-next-line no-global-assign
(console as any) = undefined;
const consoleLogger: any = new DiagConsoleLogger();
consoleLogger[fName](`${fName} called %s`, 'param1');
assert.deepStrictEqual(calledArgs[cName], null);
assert.deepStrictEqual(calledArgs.log, null);
});
it(`should not throw even when console is disabled after construction for ${fName} call`, () => {
const consoleLogger: any = new DiagConsoleLogger();
// eslint-disable-next-line no-global-assign
(console as any) = undefined;
consoleLogger[fName](`${fName} called %s`, 'param1');
assert.deepStrictEqual(calledArgs[expectedConsoleMap[fName]], null);
assert.deepStrictEqual(calledArgs.log, null);
});
it(`should not throw even when console is invalid after construction for ${fName} call`, () => {
const invalidConsole = {
debug: 1,
warn: 2,
error: 3,
trace: 4,
info: 5,
log: 6,
};
const consoleLogger = new DiagConsoleLogger();
// eslint-disable-next-line no-global-assign
(console as any) = invalidConsole;
consoleLogger[fName](`${fName} called %s`, 'param1');
assert.deepStrictEqual(calledArgs[expectedConsoleMap[fName]], null);
assert.deepStrictEqual(calledArgs.log, null);
});
it(`should not throw even when console is invalid before construction for ${fName} call`, () => {
const invalidConsole = {
debug: 1,
warn: 2,
error: 3,
trace: 4,
info: 5,
log: 6,
};
// eslint-disable-next-line no-global-assign
(console as any) = invalidConsole;
const consoleLogger = new DiagConsoleLogger();
consoleLogger[fName](`${fName} called %s`, 'param1');
assert.deepStrictEqual(calledArgs[expectedConsoleMap[fName]], null);
assert.deepStrictEqual(calledArgs.log, null);
});
});
}
});
});

View File

@ -0,0 +1,366 @@
/*
* Copyright The OpenTelemetry Authors
*
* 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
*
* https://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 { diag } from '../../src';
import { Logger } from '../../src/common/Logger';
import {
createNoopDiagLogger,
DiagLogger,
diagLoggerFunctions,
} from '../../src/diag/logger';
import {
DiagLogLevel,
createLogLevelDiagLogger,
} from '../../src/diag/logLevel';
const incompleteLoggerFuncs: Array<keyof Logger> = [
'debug',
'info',
'warn',
'error',
];
const expectedIncompleteMap: { [n: string]: keyof Console } = {
terminal: 'error',
critical: 'error',
error: 'error',
warn: 'warn',
info: 'info',
debug: 'debug',
verbose: 'debug',
startupInfo: 'info',
};
describe('LogLevelFilter DiagLogger', () => {
const calledArgs: any = {
terminal: null,
critical: null,
error: null,
warn: null,
info: null,
debug: null,
verbose: null,
startupInfo: null,
};
let dummyLogger: DiagLogger;
/** Simulated Legacy logger */
let incompleteLogger: DiagLogger;
beforeEach(() => {
// set no logger
diag.setLogger(null as any);
diag.setLogLevel(DiagLogLevel.INFO);
// mock
dummyLogger = {} as DiagLogger;
diagLoggerFunctions.forEach(fName => {
dummyLogger[fName] = (...args: unknown[]) => {
calledArgs[fName] = args;
};
});
incompleteLogger = {} as DiagLogger;
incompleteLoggerFuncs.forEach(fName => {
incompleteLogger[fName] = (...args: unknown[]) => {
calledArgs[fName] = args;
};
});
});
afterEach(() => {
// restore
diagLoggerFunctions.forEach(fName => {
calledArgs[fName] = null;
});
});
describe('constructor', () => {
const levelMap: Array<{
message: string;
level: DiagLogLevel;
ignoreFuncs: Array<keyof DiagLogger>;
}> = [
{ message: 'ALL', level: DiagLogLevel.ALL, ignoreFuncs: [] },
{ message: 'greater than ALL', level: 32768, ignoreFuncs: [] },
{ message: 'VERBOSE', level: DiagLogLevel.VERBOSE, ignoreFuncs: [] },
{ message: 'DEBUG', level: DiagLogLevel.DEBUG, ignoreFuncs: ['verbose'] },
{
message: 'INFO',
level: DiagLogLevel.INFO,
ignoreFuncs: ['verbose', 'debug'],
},
{
message: 'WARN',
level: DiagLogLevel.WARN,
ignoreFuncs: ['verbose', 'debug', 'info'],
},
{
message: 'ERROR',
level: DiagLogLevel.ERROR,
ignoreFuncs: ['verbose', 'debug', 'info', 'warn'],
},
{
message: 'CRITICAL',
level: DiagLogLevel.CRITICAL,
ignoreFuncs: [
'verbose',
'debug',
'info',
'warn',
'error',
'startupInfo',
],
},
{
message: 'TERMINAL',
level: DiagLogLevel.TERMINAL,
ignoreFuncs: [
'verbose',
'debug',
'info',
'warn',
'error',
'critical',
'startupInfo',
],
},
{
message: 'between TERMINAL and NONE',
level: 1,
ignoreFuncs: [
'verbose',
'debug',
'info',
'warn',
'error',
'critical',
'terminal',
'startupInfo',
],
},
{
message: 'NONE',
level: DiagLogLevel.NONE,
ignoreFuncs: [
'verbose',
'debug',
'info',
'warn',
'error',
'critical',
'terminal',
'startupInfo',
],
},
{
message: 'less than NONE',
level: -1000,
ignoreFuncs: [
'verbose',
'debug',
'info',
'warn',
'error',
'critical',
'terminal',
'startupInfo',
],
},
];
levelMap.forEach(map => {
diagLoggerFunctions.forEach(fName => {
it(`should log ${fName} message with ${map.message} level`, () => {
const testLogger = createLogLevelDiagLogger(map.level, dummyLogger);
testLogger[fName](`${fName} called %s`, 'param1');
diagLoggerFunctions.forEach(lName => {
if (fName === lName && map.ignoreFuncs.indexOf(lName) === -1) {
assert.deepStrictEqual(calledArgs[lName], [
`${fName} called %s`,
'param1',
]);
} else {
assert.strictEqual(calledArgs[lName], null);
}
});
});
it(`should be noop for null with explicit noop Logger log ${fName} message with ${map.message} level`, () => {
const testLogger = createLogLevelDiagLogger(
map.level,
createNoopDiagLogger()
);
testLogger[fName](`${fName} called %s`, 'param1');
diagLoggerFunctions.forEach(lName => {
assert.strictEqual(calledArgs[lName], null);
});
});
it(`should be noop and not throw for null and no default Logger log ${fName} message with ${map.message} level`, () => {
const testLogger = createLogLevelDiagLogger(map.level, null);
testLogger[fName](`${fName} called %s`, 'param1');
diagLoggerFunctions.forEach(lName => {
assert.strictEqual(calledArgs[lName], null);
});
});
it(`should be noop and not throw for undefined and no default Logger log ${fName} message with ${map.message} level`, () => {
const testLogger = createLogLevelDiagLogger(map.level, undefined);
testLogger[fName](`${fName} called %s`, 'param1');
diagLoggerFunctions.forEach(lName => {
assert.strictEqual(calledArgs[lName], null);
});
});
it(`should use default logger for undefined and log ${fName} message with ${map.message} level`, () => {
diag.setLogger(dummyLogger);
diag.setLogLevel(DiagLogLevel.ALL);
const testLogger = createLogLevelDiagLogger(map.level, undefined);
testLogger[fName](`${fName} called %s`, 'param1');
diagLoggerFunctions.forEach(lName => {
if (fName === lName && map.ignoreFuncs.indexOf(lName) === -1) {
assert.deepStrictEqual(calledArgs[lName], [
`${fName} called %s`,
'param1',
]);
} else {
assert.strictEqual(calledArgs[lName], null);
}
});
});
it(`should use default logger for null and log ${fName} message with ${map.message} level`, () => {
diag.setLogger(dummyLogger);
diag.setLogLevel(DiagLogLevel.ALL);
const testLogger = createLogLevelDiagLogger(map.level, null);
testLogger[fName](`${fName} called %s`, 'param1');
diagLoggerFunctions.forEach(lName => {
if (fName === lName && map.ignoreFuncs.indexOf(lName) === -1) {
assert.deepStrictEqual(calledArgs[lName], [
`${fName} called %s`,
'param1',
]);
} else {
assert.strictEqual(calledArgs[lName], null);
}
});
});
it(`incomplete (legacy) logger should log ${fName} message to ${expectedIncompleteMap[fName]} with ${map.message} level`, () => {
const testLogger = createLogLevelDiagLogger(
map.level,
incompleteLogger
);
testLogger[fName](`${fName} called %s`, 'param1');
diagLoggerFunctions.forEach(lName => {
const expectedLog = expectedIncompleteMap[lName];
if (fName === lName && map.ignoreFuncs.indexOf(lName) === -1) {
assert.deepStrictEqual(calledArgs[expectedLog], [
`${fName} called %s`,
'param1',
]);
}
});
});
levelMap.forEach(masterLevelMap => {
it(`diag setLogLevel is not ignored when set to ${masterLevelMap.message} and using default logger to log ${fName} message with ${map.message} level`, () => {
diag.setLogger(dummyLogger);
diag.setLogLevel(masterLevelMap.level);
const testLogger = createLogLevelDiagLogger(map.level);
testLogger[fName](`${fName} called %s`, 'param1');
diagLoggerFunctions.forEach(lName => {
if (
fName === lName &&
map.ignoreFuncs.indexOf(lName) === -1 &&
masterLevelMap.ignoreFuncs.indexOf(lName) === -1
) {
assert.deepStrictEqual(calledArgs[lName], [
`${fName} called %s`,
'param1',
]);
} else {
assert.strictEqual(calledArgs[lName], null);
}
});
});
it(`diag setLogLevel is ignored when set to ${masterLevelMap.message} when using a specific logger to log ${fName} message with ${map.message} level`, () => {
diag.setLogger(dummyLogger);
diag.setLogLevel(masterLevelMap.level);
const testLogger = createLogLevelDiagLogger(
map.level,
diag.getLogger()
);
testLogger[fName](`${fName} called %s`, 'param1');
diagLoggerFunctions.forEach(lName => {
if (fName === lName && map.ignoreFuncs.indexOf(lName) === -1) {
assert.deepStrictEqual(calledArgs[lName], [
`${fName} called %s`,
'param1',
]);
} else {
assert.strictEqual(calledArgs[lName], null);
}
});
});
});
it(`diag setLogLevel and logger should log ${fName} message with ${map.message} level`, () => {
diag.setLogger(dummyLogger);
diag.setLogLevel(map.level);
diag[fName](`${fName} called %s`, 'param1');
diagLoggerFunctions.forEach(lName => {
if (fName === lName && map.ignoreFuncs.indexOf(lName) === -1) {
assert.deepStrictEqual(calledArgs[lName], [
`${fName} called %s`,
'param1',
]);
} else {
assert.strictEqual(calledArgs[lName], null);
}
});
});
it(`should not throw with an invalid DiagLogger calling ${fName} with ${map.message} level`, () => {
const invalidLogger = {
debug: 1,
warn: 2,
error: 3,
trace: 4,
info: 5,
log: 6,
};
const testLogger = createLogLevelDiagLogger(
map.level,
invalidLogger as any
);
testLogger[fName](`${fName} called %s`, 'param1');
diagLoggerFunctions.forEach(lName => {
assert.strictEqual(calledArgs[lName], null);
});
});
});
});
});
});

View File

@ -0,0 +1,97 @@
/*
* Copyright The OpenTelemetry Authors
*
* 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
*
* https://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 { diag, DiagLogLevel } from '../../src';
import {
DiagLogger,
createNoopDiagLogger,
diagLoggerFunctions,
} from '../../src/diag/logger';
describe('DiagLogger functions', () => {
const calledArgs: any = {
terminal: null,
critical: null,
error: null,
warn: null,
info: null,
debug: null,
verbose: null,
startupInfo: null,
};
let dummyLogger: DiagLogger;
beforeEach(() => {
// mock
dummyLogger = {} as DiagLogger;
diagLoggerFunctions.forEach(fName => {
dummyLogger[fName] = (...args: unknown[]) => {
calledArgs[fName] = args;
};
});
});
afterEach(() => {
// restore
diagLoggerFunctions.forEach(fName => {
calledArgs[fName] = null;
});
});
describe('constructor', () => {
diagLoggerFunctions.forEach(fName => {
it(`should log with ${fName} message`, () => {
const testLogger = dummyLogger;
testLogger[fName](`${fName} called %s`, 'param1');
diagLoggerFunctions.forEach(lName => {
if (fName === lName) {
assert.deepStrictEqual(calledArgs[fName], [
`${fName} called %s`,
'param1',
]);
} else {
assert.strictEqual(calledArgs[lName], null);
}
});
});
it(`diag should log with ${fName} message`, () => {
diag.setLogger(dummyLogger);
diag.setLogLevel(DiagLogLevel.ALL);
diag[fName](`${fName} called %s`, 'param1');
diagLoggerFunctions.forEach(lName => {
if (fName === lName) {
assert.deepStrictEqual(calledArgs[fName], [
`${fName} called %s`,
'param1',
]);
} else {
assert.strictEqual(calledArgs[lName], null);
}
});
});
it(`NoopLogger should implement all functions and not throw when ${fName} called`, () => {
const testLogger = createNoopDiagLogger();
assert.ok(typeof testLogger[fName], 'function');
testLogger[fName](`${fName} called %s`, 'param1');
});
});
});
});

View File

@ -18,6 +18,10 @@ import { Logger } from '@opentelemetry/api';
import { LogLevel } from './types';
import { getEnv } from '../platform';
/**
* @deprecated This class will be removed prior to v1.0, use {@link DiagConsoleLogger} from the api.
* @see {@link DiagLogLevel} {@link diagLogLevelFilter} {@link DiagConsoleLogger} from the api
*/
export class ConsoleLogger implements Logger {
constructor(level: LogLevel = getEnv().OTEL_LOG_LEVEL) {
if (level >= LogLevel.DEBUG) {

View File

@ -14,9 +14,14 @@
* limitations under the License.
*/
import { Logger, Exception } from '@opentelemetry/api';
import { ConsoleLogger } from './ConsoleLogger';
import { ErrorHandler, LogLevel } from './types';
import {
Logger,
Exception,
DiagLogLevel,
DiagConsoleLogger,
createLogLevelDiagLogger,
} from '@opentelemetry/api';
import { ErrorHandler } from './types';
/**
* Returns a function that logs an error using the provided logger, or a
@ -24,7 +29,9 @@ import { ErrorHandler, LogLevel } from './types';
* @param {Logger} logger
*/
export function loggingErrorHandler(logger?: Logger): ErrorHandler {
logger = logger ?? new ConsoleLogger(LogLevel.ERROR);
logger =
logger ??
createLogLevelDiagLogger(DiagLogLevel.ERROR, new DiagConsoleLogger());
return (ex: Exception) => {
logger!.error(stringifyException(ex));
};

View File

@ -16,6 +16,9 @@
import { Exception } from '@opentelemetry/api';
/** @deprecated This enum will be removed prior to v1.0, use the {@link DiagLogLevel}
* @see {@link DiagLogLevel} from the api
*/
export enum LogLevel {
ERROR,
WARN,