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(logger): add console, cloudWatch providers #12735

Closed

Conversation

ashwinkumar6
Copy link
Contributor

@ashwinkumar6 ashwinkumar6 commented Dec 21, 2023

Description of changes

Requirements

  • Loggers should be configurable on Amplify singleton
  • Add consoleLogProvider by default, cloudWatchLogProvider is opt-in
  • CloudWatchLogger SDK should be tree shakeable and shouldn't be included in bundle if not opt-in

sample DX

Create cloudWatch service backend and add IAM permissions to auth, unauth roles.
Reference: check 'Add Amplify Logger resources' section

usage

import { createLogger } from "aws-amplify/logging";
import {
  getProvider as getConsoleProvider,
} from "aws-amplify/logging/console";
import {
  getProvider as getCloudWatchProvider,
} from "aws-amplify/logging/cloudwatch";

import { Amplify } from "aws-amplify";
import amplifyconfig from './amplifyconfiguration.json';

const cloudWatchProvider =  getCloudWatchProvider.initialize({
  logGroupName: "central-logger-poc-12-17",
  region: "us-west-2",
})

Amplify.configure(amplifyconfig,
  {
    Logger: {
      console: consoleProvider,
      additionalProviders: [cloudWatchProvider],
    },
  }
);

const logger = createLogger({namespace:"namespace", category:"Auth"});
newLogger.error("error bar");
newLogger.warn("warn bar");
newLogger.info("info bar");
newLogger.debug("debug bar");
newLogger.verbose("verbose bar");

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@ashwinkumar6 ashwinkumar6 changed the title feat(logger): add console, cloudWatch providers feat(logger): add console, cloudWatch providers (draft) Dec 21, 2023

// configure the plugin
configure(config?: object): object;
export type LogParams = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use interface if there is no specific requirements of using type alias.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks updated all type to interface except union types and some complex types

Copy link
Contributor

@jimblanc jimblanc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some nits and suggestions. The bundle size increase it a little concerning for what it is, could you do an analysis on the bundle and make sure nothing is getting included that shouldn't be?

@@ -23,6 +24,18 @@ export const DefaultAmplify = {
) {
let resolvedResourceConfig: ResourcesConfig;

// add console logger provider by default
if (!Amplify.libraryOptions?.Logger) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to move this logic into the if (libraryOptions) block below & refactor as necessary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is slightly updated to align with API review. We attached console logger by default here at the top.

Amplify.configure() is called for different scenarios :

  1. no Auth config is provided
  2. Auth options are provided
  3. add default auth providers
  4. if (libraryOptions) when Amplify.configure() is called multiple times

The reason we add it at the top is because we want logger category to be configured for all these scenarios.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from the logic at the top, if (libraryOptions) is updated as well to handle multiple Amplify.configure(). We make sure "console" provider is always configured. Customer can update console provider config (eg disable) but not remove it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slight updates on this to make sure we don't update the incoming libraryOptions params. Instead we create resolvedLibraryOptions and update that

...libraryOptions,
Logger: {
...libraryOptions?.Logger,
providers: [consoleProvider, ...customProviders],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens here if a customer also provides a consoleProvider? We might need to specifically check for that before injecting the default

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

answered in this comment

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make the Logger folder lower case to align with the others (realize it's an existing folder)?

You can do that on OSX using something like:

git mv Logger tmpLogger
git mv tmpLogger logger

Also this file should be lower-case as well.

Copy link
Contributor Author

@ashwinkumar6 ashwinkumar6 Dec 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep this is an existing folder, makes sense to make it lower case.
updated on PR #12762

renamed this file lower case on this PR

import { Amplify } from '../singleton/Amplify';
import { LogParams } from './types';

export const administrateLogger = (params: LogParams) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit-pick, but don't really care for this name. Maybe something like distributeLog or sendLogToProviders 🤔 Makes it more clear what it does

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, renamed to dispatchLogsToProviders

packages/core/src/Logger/ConsoleLogger.ts Outdated Show resolved Hide resolved
Comment on lines 7 to 9
export interface CloudWatchProvider extends LoggerProvider {
initialize: (config: CloudWatchConfig) => void;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this DX align with the API review? Isn't it supposed to be cloudWatchProvder(config)?

Copy link
Contributor Author

@ashwinkumar6 ashwinkumar6 Jan 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep the DX would be as per the design, i.e

import { 
  getConsoleProvider,
} from 'aws-amplify/logger/console';
const consoleProvider = getConsoleProvider({ defaultLogLevel: 'INFO', enable:true })

Added initialize() methods to both CloudWatchProvider and ConsoleProvider as an implementation detail. tagged this as @internal and @depricated as well

Comment on lines 22 to 24
/**
* @internal
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto about adding some comments around what this is used for, and maybe tag both of these as @deprecated out of the gate

Copy link
Contributor Author

@ashwinkumar6 ashwinkumar6 Jan 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx makes sense, Added @internal and @depricated for LOG_LEVEL getter and setter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added @internal to all other methods in console and cloudwatch providers

@@ -118,3 +118,6 @@ export {
SESSION_START_EVENT,
SESSION_STOP_EVENT,
} from './utils/sessionListener';

// logger
export { generateLogger } from './Logger';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in index.ts as it's a customer facing util (with re-export from aws-amplify)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this change.
Initially we were planning on exposing logger as a util, the plan is to expose as a category instead.

Add new logger category and it's respective apis covered in #12759

packages/core/src/types/core.ts Outdated Show resolved Hide resolved
ashwinkumar6 and others added 8 commits December 27, 2023 19:01
Co-authored-by: Jim Blanchard <jim.l.blanchard@gmail.com>
Co-authored-by: Jim Blanchard <jim.l.blanchard@gmail.com>
Co-authored-by: Jim Blanchard <jim.l.blanchard@gmail.com>
Co-authored-by: Jim Blanchard <jim.l.blanchard@gmail.com>
@ashwinkumar6 ashwinkumar6 changed the title feat(logger): add console, cloudWatch providers (draft) feat(logger): add console, cloudWatch providers Jan 2, 2024
Comment on lines 101 to 107
// console logger is always present
Logger: {
...libraryOptions?.Logger,
console:
libraryOptions?.Logger?.console ??
Amplify.libraryOptions.Logger?.console,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this block actually needed? The previous block on line 40 is checking "if there's no previously configured logger(s), then set up defaults & include any customer log providers", so wouldn't ...libraryOptions include it? I might be wrong, just trying to make sure I understand this logic since this function is so critical.

Copy link
Contributor Author

@ashwinkumar6 ashwinkumar6 Jan 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous block on line 40 is checking "if there's no previously configured logger(s), then set up defaults & include any customer log providers"

Yep that's correct, so when we do something like this for the first time both console and cloudwatch provider get added.

Amplify.configure(amplifyconfig,
  {
    Logger: {
      additionalProviders: [cloudWatchProvider],
    },
  }
);

But when we call configure again (same code above) console provider will get removed since libraryOptions is shallow merged. This isn't on the customer since they didn't configure console provider the first time.
wdyt

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, that makes sense

@ashwinkumar6 ashwinkumar6 marked this pull request as ready for review January 8, 2024 18:04
@ashwinkumar6 ashwinkumar6 requested review from a team as code owners January 8, 2024 18:04
Copy link
Contributor

@jimblanc jimblanc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, few comments & nits. Also looks like bundle size still requires some tweaking. Nothing super concerning this time as long as we know where it's coming from.

Comment on lines +2 to +4
const module = jest.requireActual('../src/logging/ConsoleLogger');
return {
ConsoleLogger: module.ConsoleLogger,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we even need to mock this if we're just using the full implementation?


/**
* Sends log event to configured logging providers
*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should tag this as @internal & @private

}

/**
* Generates a new Logger which can be used to record log events for the specified.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specified what?

Comment on lines +88 to +91
const client = new CloudWatchLogsClient({
region,
credentials: session.credentials,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay for now (since we have another task for implementing CW logger), but is this creating a new client for each log statement? I wonder if we should consider caching this & setting up a credential provider fn to keep credentials fresh.

const getConsoleLogFcn = (logLevel: LogLevel) => {
let fcn = console.log.bind(console);
switch (logLevel) {
case 'DEBUG': {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should VERBOSE map to DEBUG as well? Not sure how we treat it in the existing logger

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yesss, Great catch @jimblanc

VERBOSE logLevel should be mapped to console.debug()


const logLevelIndex = ['VERBOSE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'];

export const DEFAULT_LOG_LEVEL: LogLevel = 'WARN';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we default to WARN? 🤔 I think we use INFO in some of the function prototypes. We should also reference this value in those prototypes so it's defined in a single place.

@@ -6,6 +6,7 @@ import {
LibraryOptions,
ResourcesConfig,
defaultStorage,
consoleProvider,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure we don't need to contextualize this provider further given that it is exported from core root? E.g. consoleLoggingProvider?

@@ -1,3 +1,10 @@
jest.mock('../src/logging', () => {
const module = jest.requireActual('../src/logging/ConsoleLogger');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be doing this? Why replace with the actual module in a Hub test instead of properly mocking?

} from '@aws-amplify/core';
import { ConsoleProvider as ConsoleProviderType } from '@aws-amplify/core/src/logging/providers/console';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Shouldn't this have been ok without the aliasing?

@@ -133,7 +142,7 @@ describe('initSingleton (DefaultAmplify)', () => {

expect(mockAmplifySingletonConfigure).toHaveBeenCalledWith(
resourceConfig,
libraryOptions
{ ...libraryLoggingOptions, ...libraryOptions }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this spread order be reversed here and elsewhere below? I think it is reversed in the implementation if I'm not mistaken?

...resolvedLibraryOptions,
Logging: {
...resolvedLibraryOptions?.Logging,
// preserve console logger
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QQ: How would console logger have gotten lost here?

},
};

const getConsoleLogFcn = (logLevel: LogLevel) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Imo we should avoid the abbreviation at least in the function naming - I honestly wasn't sure what FCN stood for

Comment on lines +102 to +105
export const createLogger = (input: CreateLoggerInput): CreateLoggerOutput => {
const { namespace, category } = input;
return new Logger(namespace, category);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit

Suggested change
export const createLogger = (input: CreateLoggerInput): CreateLoggerOutput => {
const { namespace, category } = input;
return new Logger(namespace, category);
};
export const createLogger = ({ namespace, category }: CreateLoggerInput): CreateLoggerOutput => new Logger(namespace, category);

Comment on lines +44 to +45
const currentLevel =
cloudWatchConfig.loggingConstraints?.defaultLogLevel ?? DEFAULT_LOG_LEVEL;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the DEFAULT_LOG_LEVEL needed? Is it because it's possible to call log before initialize? Should it be possible to do this?

try {
session = await fetchAuthSession();
} catch (error) {
return Promise.reject('No credentials');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Probably should refactor as assertion functions?

* @internal
* @deprecated
*/
set LOG_LEVEL(level: LogLevel) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that the ConsoleLogger still needs to statically have the LOG_LEVEL but why is it necessary for these providers to too?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants