Skip to content

improv(logger): streamline Logger types #3054

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

Merged
merged 9 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 104 additions & 166 deletions packages/logger/src/Logger.ts

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions packages/logger/src/config/EnvironmentVariablesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import type { ConfigServiceInterface } from '../types/ConfigServiceInterface.js'
* These variables can be a mix of runtime environment variables set by AWS and
* variables that can be set by the developer additionally.
*
* @class
* @extends {CommonEnvironmentVariablesService}
* @see https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime
* @see https://docs.powertools.aws.dev/lambda/typescript/latest/#environment-variables
*/
Expand Down
132 changes: 100 additions & 32 deletions packages/logger/src/formatter/LogFormatter.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import type { EnvironmentVariablesService } from '../config/EnvironmentVariablesService.js';
import type {
LogAttributes,
LogFormatterInterface,
LogFormatterOptions,
} from '../types/Log.js';
import type { UnformattedAttributes } from '../types/Logger.js';
import type { LogAttributes } from '../types/Logger.js';
import type { LogFormatterOptions } from '../types/formatters.js';
import type { UnformattedAttributes } from '../types/logKeys.js';
import type { LogItem } from './LogItem.js';

/**
* This class defines and implements common methods for the formatting of log attributes.
* Class that defines and implements common methods for the formatting of log attributes.
*
* @class
* When creating a custom log formatter, you should extend this class and implement the
* {@link formatAttributes | formatAttributes()} method to define the structure of the log item.
*
* @abstract
*/
abstract class LogFormatter implements LogFormatterInterface {
abstract class LogFormatter {
/**
* EnvironmentVariablesService instance.
* If set, it allows to access environment variables.
* Instance of the {@link EnvironmentVariablesService} to use for configuration.
*/
protected envVarsService?: EnvironmentVariablesService;

Expand All @@ -24,21 +23,85 @@ abstract class LogFormatter implements LogFormatterInterface {
}

/**
* It formats key-value pairs of log attributes.
* Format key-value pairs of log attributes.
*
* You should implement this method in a subclass to define the structure of the log item.
*
* @example
* ```typescript
* import { LogFormatter, LogItem } from '@aws-lambda-powertools/logger';
* import type {
* LogAttributes,
* UnformattedAttributes,
* } from '@aws-lambda-powertools/logger/types';
*
* class MyCompanyLogFormatter extends LogFormatter {
* public formatAttributes(
* attributes: UnformattedAttributes,
* additionalLogAttributes: LogAttributes
* ): LogItem {
* const baseAttributes: MyCompanyLog = {
* message: attributes.message,
* service: attributes.serviceName,
* environment: attributes.environment,
* awsRegion: attributes.awsRegion,
* correlationIds: {
* awsRequestId: attributes.lambdaContext?.awsRequestId,
* xRayTraceId: attributes.xRayTraceId,
* },
* lambdaFunction: {
* name: attributes.lambdaContext?.functionName,
* arn: attributes.lambdaContext?.invokedFunctionArn,
* memoryLimitInMB: attributes.lambdaContext?.memoryLimitInMB,
* version: attributes.lambdaContext?.functionVersion,
* coldStart: attributes.lambdaContext?.coldStart,
* },
* logLevel: attributes.logLevel,
* timestamp: this.formatTimestamp(attributes.timestamp), // You can extend this function
* logger: {
* sampleRateValue: attributes.sampleRateValue,
* },
* };
*
* const logItem = new LogItem({ attributes: baseAttributes });
* // add any attributes not explicitly defined
* logItem.addAttributes(additionalLogAttributes);
*
* return logItem;
* }
* }
*
* @param {UnformattedAttributes} attributes - unformatted attributes
* @param {LogAttributes} additionalLogAttributes - additional log attributes
* export { MyCompanyLogFormatter };
* ```
*
* @param attributes - Unformatted attributes
* @param additionalLogAttributes - Additional log attributes
*/
public abstract formatAttributes(
attributes: UnformattedAttributes,
additionalLogAttributes: LogAttributes
): LogItem;

/**
* Format a given Error parameter.
* Format an error into a loggable object.
*
* @example
* ```json
* {
* "name": "Error",
* "location": "file.js:1",
* "message": "An error occurred",
* "stack": "Error: An error occurred\n at file.js:1\n at file.js:2\n at file.js:3",
* "cause": {
* "name": "OtherError",
* "location": "file.js:2",
* "message": "Another error occurred",
* "stack": "Error: Another error occurred\n at file.js:2\n at file.js:3\n at file.js:4"
* }
* }
* ```
*
* @param {Error} error - error to format
* @returns {LogAttributes} formatted error
* @param error - Error to format
*/
public formatError(error: Error): LogAttributes {
return {
Expand All @@ -54,11 +117,14 @@ abstract class LogFormatter implements LogFormatterInterface {
}

/**
* Format a given date into an ISO 8601 string, considering the configured timezone.
* If `envVarsService` is set and the configured timezone differs from 'UTC',
* the date is formatted to that timezone. Otherwise, it defaults to 'UTC'.
* Format a date into an ISO 8601 string with the configured timezone.
*
* If the log formatter is passed an {@link EnvironmentVariablesService} instance
* during construction, the timezone is read from the `TZ` environment variable, if present.
*
* @param {Date} now - The date to format
* Otherwise, the timezone defaults to ':UTC'.
*
* @param now - The date to format
*/
public formatTimestamp(now: Date): string {
const defaultTimezone = 'UTC';
Expand All @@ -75,9 +141,9 @@ abstract class LogFormatter implements LogFormatterInterface {
}

/**
* Get a string containing the location of an error, given a particular stack trace.
* Get the location of an error from a stack trace.
*
* @param {string} stack - stack trace
* @param stack - stack trace to parse
*/
public getCodeLocation(stack?: string): string {
if (!stack) {
Expand All @@ -100,14 +166,16 @@ abstract class LogFormatter implements LogFormatterInterface {

/**
* Create a new Intl.DateTimeFormat object configured with the specified time zone
* and formatting options. The time is displayed in 24-hour format (hour12: false).
* and formatting options.
*
* The time is displayed in 24-hour format (hour12: false).
*
* @param {string} timeZone - IANA time zone identifier (e.g., "Asia/Dhaka").
* @param timezone - IANA time zone identifier (e.g., "Asia/Dhaka").
*/
#getDateFormatter = (timeZone: string): Intl.DateTimeFormat => {
#getDateFormatter = (timezone: string): Intl.DateTimeFormat => {
const twoDigitFormatOption = '2-digit';
const validTimeZone = Intl.supportedValuesOf('timeZone').includes(timeZone)
? timeZone
const validTimeZone = Intl.supportedValuesOf('timeZone').includes(timezone)
? timezone
: 'UTC';

return new Intl.DateTimeFormat('en', {
Expand All @@ -125,12 +193,12 @@ abstract class LogFormatter implements LogFormatterInterface {
/**
* Generate an ISO 8601 timestamp string with the specified time zone and the local time zone offset.
*
* @param {Date} date - date to format
* @param {string} timeZone - IANA time zone identifier (e.g., "Asia/Dhaka").
* @param date - date to format
* @param timezone - IANA time zone identifier (e.g., "Asia/Dhaka").
*/
#generateISOTimestampWithOffset(date: Date, timeZone: string): string {
#generateISOTimestampWithOffset(date: Date, timezone: string): string {
const { year, month, day, hour, minute, second } = this.#getDateFormatter(
timeZone
timezone
)
.formatToParts(date)
.reduce(
Expand Down
37 changes: 21 additions & 16 deletions packages/logger/src/formatter/LogItem.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
import merge from 'lodash.merge';
import type { LogAttributes, LogItemInterface } from '../types/Log.js';
import type { LogAttributes } from '../types/Logger.js';

/**
* LogItem is a class that holds the attributes of a log item.
* It is used to store the attributes of a log item and to add additional attributes to it.
* It is used by the LogFormatter to store the attributes of a log item.
*
* @class
* It is used by {@link LogFormatter} to store the attributes of a log item and to add additional attributes to it.
*/
class LogItem implements LogItemInterface {
class LogItem {
/**
* The attributes of the log item.
*/
private attributes: LogAttributes = {};

/**
* Constructor for LogItem.
* @param {Object} params - The parameters for the LogItem.
* @param {LogAttributes} params.attributes - The initial attributes for the LogItem.
*
* Attributes are added in the following order:
* - Standard keys provided by the logger (e.g. `message`, `level`, `timestamp`)
* - Persistent attributes provided by developer, not formatted (done later)
* - Ephemeral attributes provided as parameters for a single log item (done later)
*
* @param params - The parameters for the LogItem.
*/
public constructor(params: { attributes: LogAttributes }) {
// Add attributes in the log item in this order:
// - Base attributes supported by the Powertool by default
// - Persistent attributes provided by developer, not formatted (done later)
// - Ephemeral attributes provided as parameters for a single log item (done later)
this.addAttributes(params.attributes);
}

/**
* Add attributes to the log item.
* @param {LogAttributes} attributes - The attributes to add to the log item.
*
* @param attributes - The attributes to add to the log item.
*/
public addAttributes(attributes: LogAttributes): this {
merge(this.attributes, attributes);
Expand All @@ -46,14 +46,18 @@ class LogItem implements LogItemInterface {

/**
* Prepare the log item for printing.
*
* This operation removes empty keys from the log item, see {@link removeEmptyKeys | removeEmptyKeys()} for more information.
*/
public prepareForPrint(): void {
this.setAttributes(this.removeEmptyKeys(this.getAttributes()));
}

/**
* Remove empty keys from the log item.
* @param {LogAttributes} attributes - The attributes to remove empty keys from.
* Remove empty keys from the log item, where empty keys are defined as keys with
* values of `undefined`, empty strings (`''`), or `null`.
*
* @param attributes - The attributes to remove empty keys from.
*/
public removeEmptyKeys(attributes: LogAttributes): LogAttributes {
const newAttributes: LogAttributes = {};
Expand All @@ -71,8 +75,9 @@ class LogItem implements LogItemInterface {
}

/**
* Set the attributes of the log item.
* @param {LogAttributes} attributes - The attributes to set for the log item.
* Replace the attributes of the log item.
*
* @param attributes - The attributes to set for the log item.
*/
public setAttributes(attributes: LogAttributes): void {
this.attributes = attributes;
Expand Down
28 changes: 17 additions & 11 deletions packages/logger/src/formatter/PowertoolsLogFormatter.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type {
LogRecordOrderKeys,
PowertoolsLogFormatterOptions,
} from '../types/formatters.js';
import type {
LogAttributes,
PowerToolsLogFormatterOptions,
PowertoolsLog,
} from '../types/Log.js';
import type { LogRecordOrder, UnformattedAttributes } from '../types/Logger.js';
PowertoolsLambdaContextKeys,
PowertoolsStandardKeys,
UnformattedAttributes,
} from '../types/logKeys.js';
import { LogFormatter } from './LogFormatter.js';
import { LogItem } from './LogItem.js';

Expand All @@ -16,13 +20,14 @@ import { LogItem } from './LogItem.js';
*/
class PowertoolsLogFormatter extends LogFormatter {
/**
* An array of keys that defines the order of the log record.
* List of keys to order log attributes by.
*
* This can be a set of keys or an array of keys.
*/
#logRecordOrder?: LogRecordOrder;
#logRecordOrder?: LogRecordOrderKeys;

public constructor(options?: PowerToolsLogFormatterOptions) {
public constructor(options?: PowertoolsLogFormatterOptions) {
super(options);

this.#logRecordOrder = options?.logRecordOrder;
}

Expand All @@ -36,7 +41,9 @@ class PowertoolsLogFormatter extends LogFormatter {
attributes: UnformattedAttributes,
additionalLogAttributes: LogAttributes
): LogItem {
const baseAttributes: PowertoolsLog = {
const baseAttributes: Partial<PowertoolsStandardKeys> &
Partial<PowertoolsLambdaContextKeys> &
LogAttributes = {
cold_start: attributes.lambdaContext?.coldStart,
function_arn: attributes.lambdaContext?.invokedFunctionArn,
function_memory_size: attributes.lambdaContext?.memoryLimitInMB,
Expand All @@ -57,8 +64,7 @@ class PowertoolsLogFormatter extends LogFormatter {
);
}

const orderedAttributes = {} as PowertoolsLog;

const orderedAttributes: LogAttributes = {};
// If logRecordOrder is set, order the attributes in the log item
for (const key of this.#logRecordOrder) {
if (key in baseAttributes && !(key in orderedAttributes)) {
Expand Down
Loading