From ac3c11e9c009a3db39ca20d7c741cdd15e9d5d88 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Mon, 4 Dec 2023 18:56:33 -0800 Subject: [PATCH] init central cloudwatch --- packages/aws-amplify/src/initSingleton.ts | 12 +++ packages/aws-amplify/src/utils/index.ts | 2 + packages/core/package.json | 1 + .../core/src/Logger/AdministrateLogger.ts | 36 ++++++++ packages/core/src/Logger/ConsoleLogger.ts | 36 +++++++- packages/core/src/Logger/index.ts | 5 +- packages/core/src/Logger/logger.ts | 83 +++++++++++++++++++ .../Logger/providers/CloudWatchProvider.ts | 80 ++++++++++++++++++ .../src/Logger/providers/ConsoleProvider.ts | 54 ++++++++++++ packages/core/src/Logger/types.ts | 51 +++++------- packages/core/src/index.ts | 7 +- packages/core/src/libraryUtils.ts | 3 + packages/core/src/singleton/Logger/types.ts | 19 +++++ packages/core/src/singleton/types.ts | 2 + packages/core/src/types/core.ts | 12 +++ yarn.lock | 42 ++++++++++ 16 files changed, 410 insertions(+), 35 deletions(-) create mode 100644 packages/core/src/Logger/AdministrateLogger.ts create mode 100644 packages/core/src/Logger/logger.ts create mode 100644 packages/core/src/Logger/providers/CloudWatchProvider.ts create mode 100644 packages/core/src/Logger/providers/ConsoleProvider.ts create mode 100644 packages/core/src/singleton/Logger/types.ts diff --git a/packages/aws-amplify/src/initSingleton.ts b/packages/aws-amplify/src/initSingleton.ts index 7398442ddfb..327c8161d62 100644 --- a/packages/aws-amplify/src/initSingleton.ts +++ b/packages/aws-amplify/src/initSingleton.ts @@ -6,6 +6,7 @@ import { LibraryOptions, ResourcesConfig, defaultStorage, + ConsoleProvider, } from '@aws-amplify/core'; import { LegacyConfig, @@ -23,6 +24,17 @@ export const DefaultAmplify = { ) { let resolvedResourceConfig: ResourcesConfig; + // add console logger provider by default + if (!Amplify.libraryOptions.Logger) { + libraryOptions = { + ...libraryOptions, + Logger: { + ...libraryOptions?.Logger, + Console: { provider: ConsoleProvider }, + }, + }; + } + if (Object.keys(resourceConfig).some(key => key.startsWith('aws_'))) { resolvedResourceConfig = parseAWSExports(resourceConfig); } else { diff --git a/packages/aws-amplify/src/utils/index.ts b/packages/aws-amplify/src/utils/index.ts index 87ff39d973f..54228195359 100644 --- a/packages/aws-amplify/src/utils/index.ts +++ b/packages/aws-amplify/src/utils/index.ts @@ -16,4 +16,6 @@ export { sessionStorage, sharedInMemoryStorage, KeyValueStorageInterface, + generateLogger, + CloudWatchProvider, } from '@aws-amplify/core'; diff --git a/packages/core/package.json b/packages/core/package.json index 0c38ce8a8dc..2ea75d0d58b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -51,6 +51,7 @@ ], "dependencies": { "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-cloudwatch-logs": "3.398.0", "@aws-sdk/types": "3.398.0", "@smithy/util-hex-encoding": "2.0.0", "@types/uuid": "^9.0.0", diff --git a/packages/core/src/Logger/AdministrateLogger.ts b/packages/core/src/Logger/AdministrateLogger.ts new file mode 100644 index 00000000000..1d5dc478aed --- /dev/null +++ b/packages/core/src/Logger/AdministrateLogger.ts @@ -0,0 +1,36 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// PENDING: logger configs to be taken from singleton +// PENDING: log filtering +import { Amplify } from '../singleton/Amplify'; +import { LogCallInputs, LogLevel } from './types'; + +const logLevelIndex = ['VERBOSE', 'DEBUG', 'INFO', 'WARN', 'ERROR']; +let logLevel: LogLevel = 'INFO'; + +export const log = (...params: LogCallInputs) => { + 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); +}; diff --git a/packages/core/src/Logger/ConsoleLogger.ts b/packages/core/src/Logger/ConsoleLogger.ts index a55e034173d..94d17920df2 100644 --- a/packages/core/src/Logger/ConsoleLogger.ts +++ b/packages/core/src/Logger/ConsoleLogger.ts @@ -1,9 +1,42 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { InputLogEvent, Logger, LoggingProvider, LogType } from './types'; import { AWS_CLOUDWATCH_CATEGORY } from '../constants'; +// TODO: cleanup these legacy logger types +// legacy logger types +enum LogType { + DEBUG = 'DEBUG', + ERROR = 'ERROR', + INFO = 'INFO', + WARN = 'WARN', + VERBOSE = 'VERBOSE', +} +interface LoggingProvider { + // return the name of you provider + getProviderName(): string; + + // return the name of you category + getCategoryName(): string; + + // configure the plugin + configure(config?: object): object; + + // take logs and push to provider + pushLogs(logs: InputLogEvent[]): void; +} +interface InputLogEvent { + timestamp: number | undefined; + message: string | undefined; +} +interface Logger { + debug(msg: string): void; + info(msg: string): void; + warn(msg: string): void; + error(msg: string): void; + addPluggable(pluggable: LoggingProvider): void; +} + const LOG_LEVELS: Record = { VERBOSE: 1, DEBUG: 2, @@ -15,6 +48,7 @@ const LOG_LEVELS: Record = { /** * Write logs * @class Logger + * @deprecated The ConsoleLogger is deprecated. Please migrate to the `logger` function. */ export class ConsoleLogger implements Logger { name: string; diff --git a/packages/core/src/Logger/index.ts b/packages/core/src/Logger/index.ts index dd924dca47d..608b283452a 100644 --- a/packages/core/src/Logger/index.ts +++ b/packages/core/src/Logger/index.ts @@ -1,4 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export { ConsoleLogger } from './ConsoleLogger'; +export { ConsoleLogger } from './ConsoleLogger'; // legacy +export { generateExternalLogger, generateInternalLogger } from './logger'; +export { logger as CloudWatchProvider } from './providers/CloudWatchProvider'; +export { logger as ConsoleProvider } from './providers/ConsoleProvider'; diff --git a/packages/core/src/Logger/logger.ts b/packages/core/src/Logger/logger.ts new file mode 100644 index 00000000000..2b1bd2c0f92 --- /dev/null +++ b/packages/core/src/Logger/logger.ts @@ -0,0 +1,83 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { LogLevel } from './types'; +import { log as centralizedLog } from './AdministrateLogger'; +import { AmplifyLoggingCategories } from '../types'; + +/** + * Write logs + * @class Logger + **/ +class Logger { + namespaces: string[]; + + constructor(namespaces: string[]) { + this.namespaces = namespaces; + } + + /** + * Write INFO log + * @method + * @memeberof Logger + * @param {string|object} msg - Logging message or object + */ + info(message: string, ...objects: any) { + this._log('INFO', message, objects); + } + + /** + * Write WARN log + * @method + * @memeberof Logger + * @param {string|object} msg - Logging message or object + */ + warn(message: string, ...objects: any) { + this._log('WARN', message, objects); + } + + /** + * Write ERROR log + * @method + * @memeberof Logger + * @param {string|object} msg - Logging message or object + */ + error(message: string, ...objects: any) { + this._log('ERROR', message, objects); + } + + /** + * Write DEBUG log + * @method + * @memeberof Logger + * @param {string|object} msg - Logging message or object + */ + debug(message: string, ...objects: any) { + this._log('DEBUG', message, objects); + } + + /** + * Write VERBOSE log + * @method + * @memeberof Logger + * @param {string|object} msg - Logging message or object + */ + verbose(message: string, ...objects: any) { + this._log('VERBOSE', message, objects); + } + + _log(logLevel: LogLevel, message: string, ...objects: any) { + centralizedLog(this.namespaces, logLevel, message, objects); + } +} + +export const generateInternalLogger = ( + forCategory: AmplifyLoggingCategories, + ...forNamespace: string[] +) => { + return new Logger([forCategory, ...forNamespace]); +}; + +export const generateExternalLogger = (...forNamespace: string[]) => { + return new Logger(forNamespace); +}; diff --git a/packages/core/src/Logger/providers/CloudWatchProvider.ts b/packages/core/src/Logger/providers/CloudWatchProvider.ts new file mode 100644 index 00000000000..b257f1db616 --- /dev/null +++ b/packages/core/src/Logger/providers/CloudWatchProvider.ts @@ -0,0 +1,80 @@ +// 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 new file mode 100644 index 00000000000..debf31f0225 --- /dev/null +++ b/packages/core/src/Logger/providers/ConsoleProvider.ts @@ -0,0 +1,54 @@ +// 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/types.ts b/packages/core/src/Logger/types.ts index 6967d1d923b..2f6eb8c821f 100644 --- a/packages/core/src/Logger/types.ts +++ b/packages/core/src/Logger/types.ts @@ -1,40 +1,27 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -/** - * Taken from @aws-sdk/client-cloudwatch-logs@3.6.1 - */ -export interface InputLogEvent { - timestamp: number | undefined; - message: string | undefined; -} +import { CloudWatchOptions, LoggerOptions } from '../singleton/Logger/types'; -export interface LoggingProvider { - // return the name of you provider - getProviderName(): string; +export type LogContext = { + namespaces: string[]; +}; - // return the name of you category - getCategoryName(): string; +export type LogLevel = 'DEBUG' | 'ERROR' | 'INFO' | 'WARN' | 'VERBOSE'; - // configure the plugin - configure(config?: object): object; +export type Logger = { + log: ( + namespaces: string[], + logLevel: LogLevel, + message: string, + objects: object[] + ) => void; +}; - // take logs and push to provider - pushLogs(logs: InputLogEvent[]): void; -} +export type LogCallInputs = Parameters; -export interface Logger { - debug(msg: string): void; - info(msg: string): void; - warn(msg: string): void; - error(msg: string): void; - addPluggable(pluggable: LoggingProvider): void; -} - -export enum LogType { - DEBUG = 'DEBUG', - ERROR = 'ERROR', - INFO = 'INFO', - WARN = 'WARN', - VERBOSE = 'VERBOSE', -} +export const isCloudWatchOptions = ( + options: CloudWatchOptions | LoggerOptions | {} +): options is CloudWatchOptions => { + return (options as CloudWatchOptions).logGroupName !== undefined; +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 05c753c7e6b..0b2957cfdb2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -70,7 +70,12 @@ export { CacheConfig } from './Cache/types'; export { I18n } from './I18n'; // Logging utilities -export { ConsoleLogger } from './Logger'; +export { + ConsoleLogger, // legacy + generateExternalLogger as generateLogger, // v6 logger + CloudWatchProvider, + ConsoleProvider, +} from './Logger'; // Service worker export { ServiceWorker } from './ServiceWorker'; diff --git a/packages/core/src/libraryUtils.ts b/packages/core/src/libraryUtils.ts index 1654bd7ced0..246c1d84467 100644 --- a/packages/core/src/libraryUtils.ts +++ b/packages/core/src/libraryUtils.ts @@ -118,3 +118,6 @@ export { SESSION_START_EVENT, SESSION_STOP_EVENT, } from './utils/sessionListener'; + +// Internal logger +export { generateInternalLogger as generateLogger } from './Logger'; diff --git a/packages/core/src/singleton/Logger/types.ts b/packages/core/src/singleton/Logger/types.ts new file mode 100644 index 00000000000..4d1352a00f9 --- /dev/null +++ b/packages/core/src/singleton/Logger/types.ts @@ -0,0 +1,19 @@ +// 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; + +export type LibraryLoggerOptions = { + [key: string]: CloudWatchOptions | LoggerOptions; +}; diff --git a/packages/core/src/singleton/types.ts b/packages/core/src/singleton/types.ts index a3787dc4b75..71e90909a24 100644 --- a/packages/core/src/singleton/types.ts +++ b/packages/core/src/singleton/types.ts @@ -12,6 +12,7 @@ import { GetCredentialsOptions, CognitoIdentityPoolConfig, } from './Auth/types'; +import { LibraryLoggerOptions } from './Logger/types'; import { GeoConfig } from './Geo/types'; import { PredictionsConfig } from './Predictions/types'; import { @@ -47,6 +48,7 @@ export type LibraryOptions = { API?: LibraryAPIOptions; Auth?: LibraryAuthOptions; Storage?: LibraryStorageOptions; + Logger?: LibraryLoggerOptions; ssr?: boolean; }; diff --git a/packages/core/src/types/core.ts b/packages/core/src/types/core.ts index b66f11462e1..0bdb1f7fddd 100644 --- a/packages/core/src/types/core.ts +++ b/packages/core/src/types/core.ts @@ -49,3 +49,15 @@ export type DelayFunction = ( args?: any[], error?: unknown ) => number | false; + +export type AmplifyLoggingCategories = + | 'Analytics' + | 'API' + | 'Authentication' + | 'DataStore' + | 'Geo' + | 'Hub' + | 'Logging' + | 'Predictions' + | 'PushNotifications' + | 'Storage'; diff --git a/yarn.lock b/yarn.lock index bc07301cf3d..273761e63da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -97,6 +97,48 @@ "@smithy/util-utf8" "^2.0.0" tslib "^2.6.2" +"@aws-sdk/client-cloudwatch-logs@3.398.0": + version "3.398.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.398.0.tgz#f34767cb51d8ea189b083a38586913fcb42a8301" + integrity sha512-uH9Ka3wZXiQlGouv72MXL01VkXsEmXzb2xIaz6VwWG2ox3pQIYCBr5qY7eP24e6dzRWyPlqvzDO4/EoR2c1bYA== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.398.0" + "@aws-sdk/credential-provider-node" "3.398.0" + "@aws-sdk/middleware-host-header" "3.398.0" + "@aws-sdk/middleware-logger" "3.398.0" + "@aws-sdk/middleware-recursion-detection" "3.398.0" + "@aws-sdk/middleware-signing" "3.398.0" + "@aws-sdk/middleware-user-agent" "3.398.0" + "@aws-sdk/types" "3.398.0" + "@aws-sdk/util-endpoints" "3.398.0" + "@aws-sdk/util-user-agent-browser" "3.398.0" + "@aws-sdk/util-user-agent-node" "3.398.0" + "@smithy/config-resolver" "^2.0.5" + "@smithy/fetch-http-handler" "^2.0.5" + "@smithy/hash-node" "^2.0.5" + "@smithy/invalid-dependency" "^2.0.5" + "@smithy/middleware-content-length" "^2.0.5" + "@smithy/middleware-endpoint" "^2.0.5" + "@smithy/middleware-retry" "^2.0.5" + "@smithy/middleware-serde" "^2.0.5" + "@smithy/middleware-stack" "^2.0.0" + "@smithy/node-config-provider" "^2.0.5" + "@smithy/node-http-handler" "^2.0.5" + "@smithy/protocol-http" "^2.0.5" + "@smithy/smithy-client" "^2.0.5" + "@smithy/types" "^2.2.2" + "@smithy/url-parser" "^2.0.5" + "@smithy/util-base64" "^2.0.0" + "@smithy/util-body-length-browser" "^2.0.0" + "@smithy/util-body-length-node" "^2.1.0" + "@smithy/util-defaults-mode-browser" "^2.0.5" + "@smithy/util-defaults-mode-node" "^2.0.5" + "@smithy/util-retry" "^2.0.0" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.5.0" + "@aws-sdk/client-comprehend@3.398.0": version "3.398.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-comprehend/-/client-comprehend-3.398.0.tgz#bfcad611fb75ab7d34b6e1a09f17a21cff003e9d"