Skip to content

Commit

Permalink
improv(logger): streamline Logger types (#3054)
Browse files Browse the repository at this point in the history
Co-authored-by: Leandro Damascena <leandro.damascena@gmail.com>
  • Loading branch information
dreamorosi and leandrodamascena authored Sep 13, 2024
1 parent c0d2158 commit db26958
Show file tree
Hide file tree
Showing 13 changed files with 630 additions and 526 deletions.
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

0 comments on commit db26958

Please sign in to comment.