Skip to content

feat(metrics): ability to set custom timestamp with setTimestamp for metrics #3310

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
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
12 changes: 12 additions & 0 deletions docs/core/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,18 @@ You can add default dimensions to your metrics by passing them as parameters in

If you'd like to remove them at some point, you can use the `clearDefaultDimensions` method.

### Changing default timestamp

When creating metrics, we use the current timestamp. If you want to change the timestamp of all the metrics you create, utilize the `setTimestamp` function. You can specify a datetime object or an integer representing an epoch timestamp in milliseconds.

Note that when specifying the timestamp using an integer, it must adhere to the epoch timezone format in milliseconds.

=== "setTimestamp method"

```typescript hl_lines="13"
--8<-- "examples/snippets/metrics/setTimestamp.ts"
```

### Flushing metrics

As you finish adding all your metrics, you need to serialize and "flush them" by calling `publishStoredMetrics()`. This will print the metrics to standard output.
Expand Down
15 changes: 15 additions & 0 deletions examples/snippets/metrics/setTimestamp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';

const metrics = new Metrics({
namespace: 'serverlessAirline',
serviceName: 'orders',
});

export const handler = async (
_event: unknown,
_context: unknown
): Promise<void> => {
const metricTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago
metrics.setTimestamp(metricTimestamp);
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
};
97 changes: 95 additions & 2 deletions packages/metrics/src/Metrics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Console } from 'node:console';
import { Utility } from '@aws-lambda-powertools/commons';
import { isDate } from 'node:util/types';
import { Utility, isIntegerNumber } from '@aws-lambda-powertools/commons';
import type {
GenericLogger,
HandlerMethodDecorator,
Expand All @@ -9,6 +10,8 @@ import { EnvironmentVariablesService } from './config/EnvironmentVariablesServic
import {
COLD_START_METRIC,
DEFAULT_NAMESPACE,
EMF_MAX_TIMESTAMP_FUTURE_AGE,
EMF_MAX_TIMESTAMP_PAST_AGE,
MAX_DIMENSION_COUNT,
MAX_METRICS_SIZE,
MAX_METRIC_VALUES_SIZE,
Expand Down Expand Up @@ -198,6 +201,11 @@ class Metrics extends Utility implements MetricsInterface {
*/
private storedMetrics: StoredMetrics = {};

/**
* Custom timestamp for the metrics
*/
#timestamp?: number;

public constructor(options: MetricsOptions = {}) {
super();

Expand Down Expand Up @@ -571,6 +579,46 @@ class Metrics extends Utility implements MetricsInterface {
this.clearMetadata();
}

/**
* Sets the timestamp for the metric.
*
* If an integer is provided, it is assumed to be the epoch time in milliseconds.
* If a Date object is provided, it will be converted to epoch time in milliseconds.
*
* The timestamp must be a Date object or an integer representing an epoch time.
* This should not exceed 14 days in the past or be more than 2 hours in the future.
* Any metrics failing to meet this criteria will be skipped by Amazon CloudWatch.
*
* See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
* See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatch-Logs-Monitoring-CloudWatch-Metrics.html
*
* @example
* ```typescript
* import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';
*
* const metrics = new Metrics({
* namespace: 'serverlessAirline',
* serviceName: 'orders',
* });
*
* export const handler = async () => {
* const metricTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago
* metrics.setTimestamp(metricTimestamp);
* metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
* };
* ```
* @param timestamp - The timestamp to set, which can be a number or a Date object.
*/
public setTimestamp(timestamp: number | Date): void {
if (!this.#validateEmfTimestamp(timestamp)) {
this.#logger.warn(
"This metric doesn't meet the requirements and will be skipped by Amazon CloudWatch. " +
'Ensure the timestamp is within 14 days in the past or up to 2 hours in the future and is also a valid number or Date object.'
);
}
this.#timestamp = this.#convertTimestampToEmfFormat(timestamp);
}

/**
* Serialize the stored metrics into a JSON object compliant with the Amazon CloudWatch EMF (Embedded Metric Format) schema.
*
Expand Down Expand Up @@ -627,7 +675,7 @@ class Metrics extends Utility implements MetricsInterface {

return {
_aws: {
Timestamp: new Date().getTime(),
Timestamp: this.#timestamp ?? new Date().getTime(),
CloudWatchMetrics: [
{
Namespace: this.namespace || DEFAULT_NAMESPACE,
Expand Down Expand Up @@ -940,6 +988,51 @@ class Metrics extends Utility implements MetricsInterface {
}
}
}

/**
* Validates a given timestamp based on CloudWatch Timestamp guidelines.
*
* Timestamp must meet CloudWatch requirements.
* The time stamp can be up to two weeks in the past and up to two hours into the future.
* See [Timestamps](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#about_timestamp)
* for valid values.
*
* @param timestamp - Date object or epoch time in milliseconds representing the timestamp to validate.
*/
#validateEmfTimestamp(timestamp: number | Date): boolean {
if (!isDate(timestamp) && !isIntegerNumber(timestamp)) {
return false;
}

const timestampMs = isDate(timestamp) ? timestamp.getTime() : timestamp;
const currentTime = new Date().getTime();

const minValidTimestamp = currentTime - EMF_MAX_TIMESTAMP_PAST_AGE;
const maxValidTimestamp = currentTime + EMF_MAX_TIMESTAMP_FUTURE_AGE;

return timestampMs >= minValidTimestamp && timestampMs <= maxValidTimestamp;
}

/**
* Converts a given timestamp to EMF compatible format.
*
* @param timestamp - The timestamp to convert, which can be either a number (in milliseconds) or a Date object.
* @returns The timestamp in milliseconds. If the input is invalid, returns 0.
*/
#convertTimestampToEmfFormat(timestamp: number | Date): number {
if (isIntegerNumber(timestamp)) {
return timestamp;
}
if (isDate(timestamp)) {
return timestamp.getTime();
}
/**
* If this point is reached, it indicates timestamp was neither a valid number nor Date
* Returning zero represents the initial date of epoch time,
* which will be skipped by Amazon CloudWatch.
**/
return 0;
}
}

export { Metrics };
12 changes: 12 additions & 0 deletions packages/metrics/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ const MAX_METRIC_VALUES_SIZE = 100;
* The maximum number of dimensions that can be added to a metric (0-indexed).
*/
const MAX_DIMENSION_COUNT = 29;
/**
* The maximum age of a timestamp in milliseconds that can be emitted in a metric.
* This is set to 14 days.
*/
const EMF_MAX_TIMESTAMP_PAST_AGE = 14 * 24 * 60 * 60 * 1000;
/**
* The maximum age of a timestamp in milliseconds that can be emitted in a metric.
* This is set to 2 hours.
*/
const EMF_MAX_TIMESTAMP_FUTURE_AGE = 2 * 60 * 60 * 1000;

/**
* The unit of the metric.
Expand Down Expand Up @@ -73,4 +83,6 @@ export {
MAX_DIMENSION_COUNT,
MetricUnit,
MetricResolution,
EMF_MAX_TIMESTAMP_PAST_AGE,
EMF_MAX_TIMESTAMP_FUTURE_AGE,
};
31 changes: 31 additions & 0 deletions packages/metrics/src/types/Metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,37 @@ interface MetricsInterface {
* @param enabled - Whether to throw an error if no metrics are emitted
*/
setThrowOnEmptyMetrics(enabled: boolean): void;
/**
* Sets the timestamp for the metric.
*
* If an integer is provided, it is assumed to be the epoch time in milliseconds.
* If a Date object is provided, it will be converted to epoch time in milliseconds.
*
* The timestamp must be a Date object or an integer representing an epoch time.
* This should not exceed 14 days in the past or be more than 2 hours in the future.
* Any metrics failing to meet this criteria will be skipped by Amazon CloudWatch.
*
* See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
* See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatch-Logs-Monitoring-CloudWatch-Metrics.html
*
* @example
* ```typescript
* import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';
*
* const metrics = new Metrics({
* namespace: 'serverlessAirline',
* serviceName: 'orders',
* });
*
* export const handler = async () => {
* const metricTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago
* metrics.setTimestamp(metricTimestamp);
* metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
* };
* ```
* @param timestamp - The timestamp to set, which can be a number or a Date object.
*/
setTimestamp(timestamp: number | Date): void;
/**
* Create a new Metrics instance configured to immediately flush a single metric.
*
Expand Down
Loading