Skip to content

RFC: Support for external observability providers - Logging #1500

Closed
@erikayao93

Description

Is this related to an existing feature request or issue?

Powertools for AWS Lambda (Python) #2014, #1261, #646

Which Powertools for AWS Lambda (TypeScript) utility does this relate to?

Logger

Summary

Powertools for Python has implemented support for certain external observability providers out of the box for the Logger utility by extending the LogFormatter feature according to Powertools for AWS Lambda (Python) #2014. This allows customers using designated providers to use the Logger utility with either the default or a custom LogFormatter without having to design their own configurations to allow for integration with observability solutions other than CloudWatch.

In Powertools for TypeScript, the current LogFormatter lacks customizability on certain features, as described in issue #1261. By extending the capabilities of the LogFormatter to accommodate changes on all log features, we aim to better support third-party observability providers, starting with Datadog, and expanding to encompass the same list of providers as Powertools for Python.

Use case

The primary use case for this utility will be for customers who might want to extend the default LogFormatter to fully customize the logs emitted by Logger so that they can conform with their requirements or better work with supported AWS Lambda Ready Partners and AWS Partners observability providers.

Proposal

Terminology

  • Standard attributes — keys provided to structured logging by the default log formatter
  • Context-related attributes — added to standard attributes if developer uses injectLambdaContext() or calls the addContext() method for their logger
  • Base attributes — standard attributes and context related attributes that are currently handled by the formatter
  • Persistent attributes — defined by the developer and persists through all instances of logger output
  • Extra Input attributes — provided for logger output on a singular instance

Current LogFormatter Experience

Powertools for Typescript currently provides a base abstract class LogFormatter with the abstract method formatAttributes that must be implemented to define the formatting of base attributes:

public abstract formatAttributes(
    attributes: UnformattedAttributes
): LogAttributes;

Our default implementation for the Logger uses the PowertoolLogFormatter class that builds on top of the LogFormatter abstract class, maintaining its members and methods. The class provides the following implementation for the formatAttributes method:

public formatAttributes(attributes: UnformattedAttributes): PowertoolLog {
    return {
      cold_start: attributes.lambdaContext?.coldStart,
      function_arn: attributes.lambdaContext?.invokedFunctionArn,
      function_memory_size: attributes.lambdaContext?.memoryLimitInMB,
      function_name: attributes.lambdaContext?.functionName,
      function_request_id: attributes.lambdaContext?.awsRequestId,
      level: attributes.logLevel,
      message: attributes.message,
      sampling_rate: attributes.sampleRateValue,
      service: attributes.serviceName,
      timestamp: this.formatTimestamp(attributes.timestamp),
      xray_trace_id: attributes.xRayTraceId,
    };
}

This block of code provides default formatting for standard attributes and context-related ones if present. This formatted list of attributes is of type PowertoolLog, which is an alias for a specific list of LogAttributes.

If customers wish to customize the formatting to meet their own requirements or to work with third-party observability providers, they can define a similar class to alter the formatting on these standard and context-related attributes:

class MyCompanyLogFormatter extends LogFormatter {
  public formatAttributes(attributes: UnformattedAttributes): MyCompanyLog {
    return {
      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),
      logger: {
        sampleRateValue: attributes.sampleRateValue,
      },
    };
  }
}

export { MyCompanyLogFormatter };

Important to note here is the lack of functionality that is provided for altering persistent attributes or extra attributes. Instead, for persistent attributes added to the logger, it maintains the formatting that the user provided the attributes in. These are aggregated as another set of LogAttributes.

Both sets of LogAttributes (one for the standard attributes and context-related ones and one for the persistent attributes) are combined in the constructor for a LogItem, which has a private member attributes that is a list of LogAttributes.

public constructor(params: {
    baseAttributes: LogAttributes;
    persistentAttributes: LogAttributes;
  }) {
    this.addAttributes(params.baseAttributes);
    this.addAttributes(params.persistentAttributes);
}

The benefit of the LogItem is that it also possesses methods for adding, getting, deleting, and setting attributes. However, the addAttributes method simply merges a new set of logAttributes into attributes of logItem. This means that customers do not have the ability to reorganize their persistent attributes with the formatted list of base attributes, nor do they have the ability to reformat the persistent attributes, and the list is simply appended to the end of the attributes list.

For extra attributes, these attributes are converted into a set of logAttributes and added to the attributes list with the following code:

extraInput.forEach((item: Error | LogAttributes | string) => {
  const attributes: LogAttributes =
     item instanceof Error
       ? { error: item }
       : typeof item === 'string'
       ? { extra: item }
       : item;

  logItem.addAttributes(attributes);
});

Similar to the persistent attributes, the extraInput formatting here does not provide customers with the ability to reformat or reorganize the attributes with their existing list of attributes for the log. The new attributes are merely appended to the end of the attributes list, after the base attributes and persistent attributes.

Proposed PowertoolsLogFormatter Update

Firstly, to maintain naming consistency, PowertoolLogFormatter and PowertoolLog will be renamed to PowertoolsLogFormatter and PowertoolsLog. If this inconsistency is discovered in other parts of the Logger utility through the implementation of this proposal, the name will also be updated in those instances.

Instead of having persistent attributes and extra attributes merged into a LogItem after the base attributes have been formatted, we propose merging the persistent attributes and extra attributes first, into a set of logAttributes that can be passed into formatAttributes instead:

var additionalLogAttributes: LogAttributes = this.getPersistentLogAttributes();
if (typeof input !== 'string') {
    additionalLogAttributes = merge(additionalLogAttributes, input);
}
extraInput.forEach((item: Error | LogAttributes | string) => {
    const attributes: LogAttributes =
    item instanceof Error
        ? { error: item }
        : typeof item === 'string'
        ? { extra: item }
        : item;

    additionalLogAttributes = merge(additionalLogAttributes, attributes);
});

This functionality will be maintained in the Logger file, where the LogItem used to be created.

Then, to provide customers with access to formatting beyond the base attributes, we propose extending the definition of the base LogFormatter class and its formatAttributes method to access the original unformatted baseAttributes and the new additionalLogAttributes set:

public abstract formatAttributes(
    attributes: UnformattedAttributes,
    additionalLogAttributes: LogAttributes
  ): LogItem;

In order to maintain a mask on the merging functionality provided in the LogItem class, we will utilize addAttributes, as defined in LogItem, and formatAttributes will be redefined to generate a LogItem object rather than LogAttributes.

Within the PowertoolsLogFormatter, the formatAttributes function will be redefined in accordance to the LogFormatter update to take in all attributes, not just the base attributes:

public formatAttributes(attributes: UnformattedAttributes, 
                          additionalLogAttributes: LogAttributes): LogItem {

    const baseAttributes: PowertoolsLog = {
      cold_start: attributes.lambdaContext?.coldStart,
      function_arn: attributes.lambdaContext?.invokedFunctionArn,
      function_memory_size: attributes.lambdaContext?.memoryLimitInMB,
      function_name: attributes.lambdaContext?.functionName,
      function_request_id: attributes.lambdaContext?.awsRequestId,
      level: attributes.logLevel,
      message: attributes.message,
      sampling_rate: attributes.sampleRateValue,
      service: attributes.serviceName,
      timestamp: this.formatTimestamp(attributes.timestamp),
      xray_trace_id: attributes.xRayTraceId,
    };

    const powertoolsLogItem = new LogItem({attributes: baseAttributes});

    powertoolsLogItem.addAttributes(additionalLogAttributes);

    return powertoolsLogItem;
}

We maintain the attributes of PowertoolsLog but maintain the customizability of adding fields the same way current functionality does.

For a LogItem object to be created from this, we will have to redefine the constructor of LogItem to construct off a single attribute list. This will allow LogItem to be more flexible in terms of its initial definition, while maintaining that other attributes can always be added or deleted later using its member functions.

public constructor(params: {
    attributes: LogAttributes;
}) {
    this.addAttributes(params.attributes);
}

Custom LogFormatter Usage

If the customer would like to use another observability provider, or define their own logger functions, customers can define their own Formatter class that the customer can implement based on our existing framework and pass in to the Logger class. The class would implement the formatAttributes method similar to:

public formatAttributes(attributes: UnformattedAttributes, 
                          additionalLogAttributes: LogAttributes): LogItem {
    
    const attributeList: CustomLog = {
        cold_start: attributes.lambdaContext?.coldStart,
        function_arn: attributes.lambdaContext?.invokedFunctionArn,
        function_memory_size: attributes.lambdaContext?.memoryLimitInMB,
        function_name: attributes.lambdaContext?.functionName,
        function_request_id: attributes.lambdaContext?.awsRequestId,
        level: attributes.logLevel,
        message: attributes.message,
        sampling_rate: attributes.sampleRateValue,
        service: attributes.serviceName,
        timestamp: this.formatTimestamp(attributes.timestamp),
        xray_trace_id: attributes.xRayTraceId,
        custom_persistent_field = additionalLogAttributes.customPersistentField,
        custom_error_field = additionalLogAttributes.myError,
        custom_extra_field = additionalLogAttributes.customExtraField
    }
    
    const logItem = new LogItem({attributes: attributeList});

    return logItem;
}

If customers wish to, they can define their own CustomLog type to use as shown above that will pre-establish the fields of attributeList, similar to the PowertoolsLog type that the PowertoolsLogFormatter uses for the base attributes.

Out of scope

Sending logs from Powertools to the customer's desired observability platform will not be in the scope of this project. The implementation should only support modifying the output of the Logger so that the customer can push them to their platform of choice.

Potential challenges

These proposed changes will likely result in a breaking change, which will require a major release and a migration plan/guide.

By reorganizing the PowertoolsLogFormatter, some functionality that is currently contained in LogItem may be removed or migrated to the Formatters. We will have to identify other dependencies of the LogItem class to ensure that other functionality is not lost.

We need to determine which platforms we want to support out-of-the-box.

Dependencies and Integrations

We will have to integrate with (and thus, have a dependency on) the platforms we decide to support out-of-the-box. For now, we will be focusing on Datadog, but this should be expanded to encompass the same providers supported by Powertools for Python, listed here.

Alternative solutions

To optimize the merging of LogAttributes sets, we can consider migrating the removeEmptyKeys() method from LogItem to be used on small sets of attributes before they are merged together.

We have considered several different ways of handling passing the persistent attributes and extra attributes to the formatter:

  • Combine persistent attributes and extra attributes into two separate sets of LogAttributes to pass into formatAttributes
    • As discussed by @dreamorosi in this comment, both types of attributes are treated equally anyways, and there isn't much reason to treat them separately.
  • Pass persistent attributes and extra attributes in their raw form, without converting extra attributes into LogAttributes, so that customers can merge them into their log however they wish
    • The merging functionality ideally should remain masked from customers and remain in the Logger as much as possible, without encroaching on the more straightforward functionality for the formatter

Acknowledgment

Future readers

Please react with 👍 and your use case to help us understand customer demand.

Metadata

Assignees

Labels

RFCTechnical design documents related to a feature requestcompletedThis item is complete and has been merged/shippedloggerThis item relates to the Logger Utility

Type

No type

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions