diff --git a/packages/aws-amplify/src/initSingleton.ts b/packages/aws-amplify/src/initSingleton.ts index 327c8161d62..445917faace 100644 --- a/packages/aws-amplify/src/initSingleton.ts +++ b/packages/aws-amplify/src/initSingleton.ts @@ -6,7 +6,7 @@ import { LibraryOptions, ResourcesConfig, defaultStorage, - ConsoleProvider, + consoleProvider, } from '@aws-amplify/core'; import { LegacyConfig, @@ -25,12 +25,13 @@ export const DefaultAmplify = { let resolvedResourceConfig: ResourcesConfig; // add console logger provider by default - if (!Amplify.libraryOptions.Logger) { + if (!Amplify.libraryOptions?.Logger) { + const customProviders = libraryOptions?.Logger?.providers ?? []; libraryOptions = { ...libraryOptions, Logger: { ...libraryOptions?.Logger, - Console: { provider: ConsoleProvider }, + providers: [consoleProvider, ...customProviders], }, }; } diff --git a/packages/aws-amplify/src/utils/index.ts b/packages/aws-amplify/src/utils/index.ts index 54228195359..ee48732f820 100644 --- a/packages/aws-amplify/src/utils/index.ts +++ b/packages/aws-amplify/src/utils/index.ts @@ -17,5 +17,6 @@ export { sharedInMemoryStorage, KeyValueStorageInterface, generateLogger, - CloudWatchProvider, + cloudWatchProvider, + consoleProvider, } from '@aws-amplify/core'; diff --git a/packages/core/src/Logger/AdministrateLogger.ts b/packages/core/src/Logger/AdministrateLogger.ts index 1d5dc478aed..bd6c84d0002 100644 --- a/packages/core/src/Logger/AdministrateLogger.ts +++ b/packages/core/src/Logger/AdministrateLogger.ts @@ -4,33 +4,10 @@ // PENDING: logger configs to be taken from singleton // PENDING: log filtering import { Amplify } from '../singleton/Amplify'; -import { LogCallInputs, LogLevel } from './types'; +import { LogParams } from './types'; -const logLevelIndex = ['VERBOSE', 'DEBUG', 'INFO', 'WARN', 'ERROR']; -let logLevel: LogLevel = 'INFO'; - -export const log = (...params: LogCallInputs) => { +export const administrateLogger = (params: LogParams) => { const loggers = Amplify.libraryOptions.Logger; if (loggers) - Object.values(loggers).forEach(logger => logger.provider.log(...params)); -}; - -export const setLogLevel = (level: LogLevel) => { - logLevel = level; -}; - -export const getLogLevel = (): LogLevel => { - if (typeof (window) !== 'undefined' && (window).LOG_LEVEL) { - const windowLog = (window).LOG_LEVEL; - if (logLevelIndex.includes(windowLog)) return windowLog; - } - return logLevel; -}; - -export const checkLogLevel = ( - level: LogLevel, - setLevel: LogLevel | undefined = undefined -): boolean => { - const targetLevel = setLevel ?? getLogLevel(); - return logLevelIndex.indexOf(level) >= logLevelIndex.indexOf(targetLevel); + Object.values(loggers.providers).forEach(logger => logger.log(params)); }; diff --git a/packages/core/src/Logger/ConsoleLogger.ts b/packages/core/src/Logger/ConsoleLogger.ts index 94d17920df2..23743271932 100644 --- a/packages/core/src/Logger/ConsoleLogger.ts +++ b/packages/core/src/Logger/ConsoleLogger.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AWS_CLOUDWATCH_CATEGORY } from '../constants'; +import { consoleProvider } from './providers/console'; // TODO: cleanup these legacy logger types // legacy logger types @@ -66,7 +67,14 @@ export class ConsoleLogger implements Logger { this._pluggables = []; } - static LOG_LEVEL: string | null = null; + private static _logLevel: string | null = null; + static get LOG_LEVEL(): string | null { + return this._logLevel; + } + static set LOG_LEVEL(level: string | null) { + consoleProvider.LOG_LEVEL = level; + this._logLevel = level; + } _padding(n: number) { return n < 10 ? '0' + n : '' + n; diff --git a/packages/core/src/Logger/index.ts b/packages/core/src/Logger/index.ts index 608b283452a..ca42faedc7b 100644 --- a/packages/core/src/Logger/index.ts +++ b/packages/core/src/Logger/index.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 export { ConsoleLogger } from './ConsoleLogger'; // legacy +export { LoggerProvider } from './types'; export { generateExternalLogger, generateInternalLogger } from './logger'; -export { logger as CloudWatchProvider } from './providers/CloudWatchProvider'; -export { logger as ConsoleProvider } from './providers/ConsoleProvider'; +export { cloudWatchProvider } from './providers/cloudWatch'; +export { consoleProvider } from './providers/console'; diff --git a/packages/core/src/Logger/logger.ts b/packages/core/src/Logger/logger.ts index 2b1bd2c0f92..64fee8479b8 100644 --- a/packages/core/src/Logger/logger.ts +++ b/packages/core/src/Logger/logger.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { LogLevel } from './types'; -import { log as centralizedLog } from './AdministrateLogger'; +import { administrateLogger } from './AdministrateLogger'; import { AmplifyLoggingCategories } from '../types'; /** @@ -22,8 +22,8 @@ class Logger { * @memeberof Logger * @param {string|object} msg - Logging message or object */ - info(message: string, ...objects: any) { - this._log('INFO', message, objects); + info(message: string) { + this._log('INFO', message); } /** @@ -32,8 +32,8 @@ class Logger { * @memeberof Logger * @param {string|object} msg - Logging message or object */ - warn(message: string, ...objects: any) { - this._log('WARN', message, objects); + warn(message: string) { + this._log('WARN', message); } /** @@ -42,8 +42,8 @@ class Logger { * @memeberof Logger * @param {string|object} msg - Logging message or object */ - error(message: string, ...objects: any) { - this._log('ERROR', message, objects); + error(message: string) { + this._log('ERROR', message); } /** @@ -52,8 +52,8 @@ class Logger { * @memeberof Logger * @param {string|object} msg - Logging message or object */ - debug(message: string, ...objects: any) { - this._log('DEBUG', message, objects); + debug(message: string) { + this._log('DEBUG', message); } /** @@ -62,12 +62,12 @@ class Logger { * @memeberof Logger * @param {string|object} msg - Logging message or object */ - verbose(message: string, ...objects: any) { - this._log('VERBOSE', message, objects); + verbose(message: string) { + this._log('VERBOSE', message); } - _log(logLevel: LogLevel, message: string, ...objects: any) { - centralizedLog(this.namespaces, logLevel, message, objects); + _log(logLevel: LogLevel, message: string) { + administrateLogger({ namespaces: this.namespaces, logLevel, message }); } } diff --git a/packages/core/src/Logger/providers/CloudWatchProvider.ts b/packages/core/src/Logger/providers/CloudWatchProvider.ts deleted file mode 100644 index b257f1db616..00000000000 --- a/packages/core/src/Logger/providers/CloudWatchProvider.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -// PENDING: complete implementation of CloudWatchProvider -import { Amplify } from '../../singleton/Amplify'; -import { fetchAuthSession } from '../../index'; -import { - CloudWatchLogsClient, - PutLogEventsCommand, -} from '@aws-sdk/client-cloudwatch-logs'; -import { checkLogLevel } from '../AdministrateLogger'; -import { LogLevel, Logger, isCloudWatchOptions } from '../types'; - -export const logger: Logger = { - log: ( - namespaces: string[], - logLevel: LogLevel, - message: string, - objects: object[] - ) => { - const timestamp = getTimestamp(); - const prefix = `[${logLevel}] ${timestamp} ${namespaces.join(' - ')}`; - - // todo: add timestamp, object - if (checkLogLevel(logLevel)) - putLogEvents({ message: `${prefix} ${message}` }); - }, -}; - -async function putLogEvents({ - message, - timestamp, -}: { - message: string; - timestamp?: number; -}) { - try { - // TODO assert errors for required items - const config = Amplify.libraryOptions?.Logger?.CloudWatch; - - // add assert instead - if (!config || !isCloudWatchOptions(config)) return; - const { logGroupName, logStreamName, region } = config; - - let session; - try { - session = await fetchAuthSession(); - } catch (error) { - return Promise.reject('No credentials'); - } - const client = new CloudWatchLogsClient({ - region, - credentials: session.credentials, - }); - - const timestamp = new Date().getTime(); - const params = { - logEvents: [{ message, timestamp }], - logGroupName, - logStreamName, - }; - - await client.send(new PutLogEventsCommand(params)); - } catch (error) { - console.error('Error putting log events:', error); - } -} - -const getTimestamp = () => { - const dt = new Date(); - return ( - [padding(dt.getMinutes()), padding(dt.getSeconds())].join(':') + - '.' + - dt.getMilliseconds() - ); -}; - -const padding = (n: number) => { - return n < 10 ? '0' + n : '' + n; -}; diff --git a/packages/core/src/Logger/providers/ConsoleProvider.ts b/packages/core/src/Logger/providers/ConsoleProvider.ts deleted file mode 100644 index debf31f0225..00000000000 --- a/packages/core/src/Logger/providers/ConsoleProvider.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -// PENDING: complete implementation of ConsoleProvider -import { checkLogLevel } from '../AdministrateLogger'; -import { LogLevel, Logger } from '../types'; -export const logger: Logger = { - log: ( - namespaces: string[], - logLevel: LogLevel, - message: string, - objects: object[] - ) => { - const logFcn = getConsoleLogFcn(logLevel); - const prefix = `[${logLevel}] ${timestamp()} ${namespaces.join(' - ')}`; - if (checkLogLevel(logLevel)) logFcn(prefix, message, ...objects); - }, -}; - -const getConsoleLogFcn = (logLevel: LogLevel) => { - let fcn = console.log.bind(console); - switch (logLevel) { - case 'DEBUG': { - fcn = console.debug?.bind(console) ?? fcn; - break; - } - case 'ERROR': { - fcn = console.error?.bind(console) ?? fcn; - break; - } - case 'INFO': { - fcn = console.info?.bind(console) ?? fcn; - break; - } - case 'WARN': { - fcn = console.warn?.bind(console) ?? fcn; - break; - } - } - return fcn; -}; - -const padding = (n: number) => { - return n < 10 ? '0' + n : '' + n; -}; - -const timestamp = () => { - const dt = new Date(); - return ( - [padding(dt.getMinutes()), padding(dt.getSeconds())].join(':') + - '.' + - dt.getMilliseconds() - ); -}; diff --git a/packages/core/src/Logger/providers/cloudWatch.ts b/packages/core/src/Logger/providers/cloudWatch.ts new file mode 100644 index 00000000000..55175fccf51 --- /dev/null +++ b/packages/core/src/Logger/providers/cloudWatch.ts @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// PENDING: complete implementation of cloudWatchProvider +import { Amplify } from '../../singleton/Amplify'; +import { fetchAuthSession } from '../../index'; +import { LoggerProvider, CloudWatchConfig, LogParams } from '../types'; +import { checkLogLevel, getTimestamp, DEFAULT_LOG_LEVEL } from '../utils'; +import { + CloudWatchLogsClient, + PutLogEventsCommand, +} from '@aws-sdk/client-cloudwatch-logs'; +// import { generateRandomString } from '@aws-amplify/core/internals/utils'; + +let cloudWatchConfig: CloudWatchConfig; +const defaultConfig = { + enable: true, + localStoreMaxSizeInMB: 5, + flushIntervalInSeconds: 60, + loggingConstraints: { + defaultLogLevel: DEFAULT_LOG_LEVEL, + }, +}; + +export const cloudWatchProvider: LoggerProvider = { + initialize: (config: CloudWatchConfig) => { + if (cloudWatchConfig) + throw new Error('CloudWatch provider already initialised'); + cloudWatchConfig = { ...defaultConfig, ...config }; + }, + log: (input: LogParams) => { + const { namespaces, logLevel, message } = input; + const date = new Date(); + const timestamp = getTimestamp(date); + const prefix = `[${logLevel}] ${timestamp} ${namespaces.join(' - ')}`; + const currentLevel = + cloudWatchConfig.loggingConstraints?.defaultLogLevel ?? DEFAULT_LOG_LEVEL; + + if (checkLogLevel(logLevel, currentLevel)) + putLogEvents(`${prefix} ${message}`, date.getTime()); + }, + flushLogs: function (): Promise { + return Promise.resolve(); // todo + }, +}; + +async function putLogEvents(message: string, timestamp: number) { + try { + const { logGroupName, region } = cloudWatchConfig; + // todo: {mm-dd-yyyy}.{deviceId}.{userId|guest} + // const logStreamName = generateRandomString(10); + const logStreamName = 'central-logger-12-17-2023'; + + let session; + try { + session = await fetchAuthSession(); + } catch (error) { + return Promise.reject('No credentials'); + } + const client = new CloudWatchLogsClient({ + region, + credentials: session.credentials, + }); + + const params = { + logEvents: [{ message, timestamp }], + logGroupName, + logStreamName, + }; + + await client.send(new PutLogEventsCommand(params)); + } catch (error) { + console.error('Error putting log events:', error); + } +} diff --git a/packages/core/src/Logger/providers/console.ts b/packages/core/src/Logger/providers/console.ts new file mode 100644 index 00000000000..bc596adf64a --- /dev/null +++ b/packages/core/src/Logger/providers/console.ts @@ -0,0 +1,61 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// PENDING: complete implementation of ConsoleProvider +import { ConsoleConfig, LogLevel, LogParams, ConsoleProvider } from '../types'; +import { getTimestamp, checkLogLevel, DEFAULT_LOG_LEVEL } from '../utils'; + +let consoleConfig: ConsoleConfig; +const defaultConfig = { + enable: true, + defaultLogLevel: DEFAULT_LOG_LEVEL, +}; + +export const consoleProvider: ConsoleProvider = { + get LOG_LEVEL(): LogLevel { + if (!consoleConfig) consoleProvider.initialize(defaultConfig); + return consoleConfig.defaultLogLevel; + }, + set LOG_LEVEL(level: LogLevel) { + if (!consoleConfig) consoleProvider.initialize(defaultConfig); + consoleConfig.defaultLogLevel = level; + }, + initialize: (config: ConsoleConfig = defaultConfig) => { + if (consoleConfig) throw new Error('Console provider already initialised'); + consoleConfig = config; + }, + log: (input: LogParams) => { + if (!consoleConfig) consoleProvider.initialize(defaultConfig); + const { namespaces, logLevel, message } = input; + const logFcn = getConsoleLogFcn(logLevel); + const prefix = `[${logLevel}] ${getTimestamp()} ${namespaces.join(' - ')}`; + if (checkLogLevel(logLevel, consoleConfig.defaultLogLevel)) + logFcn(prefix, message); + }, + flushLogs: function (): Promise { + return Promise.resolve(); + }, +}; + +const getConsoleLogFcn = (logLevel: LogLevel) => { + let fcn = console.log.bind(console); + switch (logLevel) { + case 'DEBUG': { + fcn = console.debug?.bind(console) ?? fcn; + break; + } + case 'ERROR': { + fcn = console.error?.bind(console) ?? fcn; + break; + } + case 'INFO': { + fcn = console.info?.bind(console) ?? fcn; + break; + } + case 'WARN': { + fcn = console.warn?.bind(console) ?? fcn; + break; + } + } + return fcn; +}; diff --git a/packages/core/src/Logger/types.ts b/packages/core/src/Logger/types.ts index 2f6eb8c821f..2735f4f1ec1 100644 --- a/packages/core/src/Logger/types.ts +++ b/packages/core/src/Logger/types.ts @@ -1,27 +1,37 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { CloudWatchOptions, LoggerOptions } from '../singleton/Logger/types'; +export type ConsoleConfig = { + enable: boolean; + defaultLogLevel: LogLevel; +}; + +export type CloudWatchConfig = { + enable?: boolean; + logGroupName: string; + region: string; + localStoreMaxSizeInMB?: number; + flushIntervalInSeconds?: number; + loggingConstraints?: { + defaultLogLevel: LogLevel; + }; +}; -export type LogContext = { +export type LogParams = { namespaces: string[]; + logLevel: LogLevel; + message: string; }; export type LogLevel = 'DEBUG' | 'ERROR' | 'INFO' | 'WARN' | 'VERBOSE'; -export type Logger = { - log: ( - namespaces: string[], - logLevel: LogLevel, - message: string, - objects: object[] - ) => void; -}; - -export type LogCallInputs = Parameters; +export interface LoggerProvider { + initialize: (config: ProviderConfig) => void; + log: (logParams: LogParams) => void; // namespaces, category ? + flushLogs: () => Promise; +} -export const isCloudWatchOptions = ( - options: CloudWatchOptions | LoggerOptions | {} -): options is CloudWatchOptions => { - return (options as CloudWatchOptions).logGroupName !== undefined; -}; +export interface ConsoleProvider extends LoggerProvider { + get LOG_LEVEL(): string | null; + set LOG_LEVEL(level: string | null); +} diff --git a/packages/core/src/Logger/utils/index.ts b/packages/core/src/Logger/utils/index.ts new file mode 100644 index 00000000000..e0b788738d7 --- /dev/null +++ b/packages/core/src/Logger/utils/index.ts @@ -0,0 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export { getTimestamp } from './timeUitls'; +export { checkLogLevel, DEFAULT_LOG_LEVEL } from './loglevel'; diff --git a/packages/core/src/Logger/utils/loglevel.ts b/packages/core/src/Logger/utils/loglevel.ts new file mode 100644 index 00000000000..704278f8e68 --- /dev/null +++ b/packages/core/src/Logger/utils/loglevel.ts @@ -0,0 +1,17 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { LogLevel } from '../types'; + +const logLevelIndex = ['VERBOSE', 'DEBUG', 'INFO', 'WARN', 'ERROR']; + +export const DEFAULT_LOG_LEVEL: LogLevel = 'WARN'; + +export const checkLogLevel = ( + inputLevel: LogLevel, + currentLevel: LogLevel +): boolean => { + return ( + logLevelIndex.indexOf(inputLevel) >= logLevelIndex.indexOf(currentLevel) + ); +}; diff --git a/packages/core/src/Logger/utils/timeUitls.ts b/packages/core/src/Logger/utils/timeUitls.ts new file mode 100644 index 00000000000..150a766487f --- /dev/null +++ b/packages/core/src/Logger/utils/timeUitls.ts @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const getTimestamp = (date?: Date) => { + const dt = date ?? new Date(); + return ( + [padding(dt.getMinutes()), padding(dt.getSeconds())].join(':') + + '.' + + dt.getMilliseconds() + ); +}; + +const padding = (n: number) => { + return n < 10 ? '0' + n : '' + n; +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0b2957cfdb2..36485715748 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -71,10 +71,10 @@ export { I18n } from './I18n'; // Logging utilities export { - ConsoleLogger, // legacy - generateExternalLogger as generateLogger, // v6 logger - CloudWatchProvider, - ConsoleProvider, + ConsoleLogger, + generateExternalLogger as generateLogger, + cloudWatchProvider, + consoleProvider, } from './Logger'; // Service worker diff --git a/packages/core/src/singleton/Logger/types.ts b/packages/core/src/singleton/Logger/types.ts index 4d1352a00f9..0dbd4294855 100644 --- a/packages/core/src/singleton/Logger/types.ts +++ b/packages/core/src/singleton/Logger/types.ts @@ -1,19 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// PENDING: update public interface based on design doc -import { Logger } from '../../Logger/types'; - -export type LoggerOptions = { - provider: Logger; -}; - -export type CloudWatchOptions = { - logGroupName: string; - logStreamName: string; - region: string; -} & LoggerOptions; +import { LoggerProvider } from '../../Logger/types'; export type LibraryLoggerOptions = { - [key: string]: CloudWatchOptions | LoggerOptions; + providers: [LoggerProvider, ...LoggerProvider[]]; };