Skip to content

Commit 0fb94c3

Browse files
authored
feat(metrics): ability to set custom timestamp with setTimestamp for metrics (#3310)
1 parent 1766d0d commit 0fb94c3

File tree

6 files changed

+425
-3
lines changed

6 files changed

+425
-3
lines changed

Diff for: docs/core/metrics.md

+12
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,18 @@ You can add default dimensions to your metrics by passing them as parameters in
229229

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

232+
### Changing default timestamp
233+
234+
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.
235+
236+
Note that when specifying the timestamp using an integer, it must adhere to the epoch timezone format in milliseconds.
237+
238+
=== "setTimestamp method"
239+
240+
```typescript hl_lines="13"
241+
--8<-- "examples/snippets/metrics/setTimestamp.ts"
242+
```
243+
232244
### Flushing metrics
233245

234246
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.

Diff for: examples/snippets/metrics/setTimestamp.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';
2+
3+
const metrics = new Metrics({
4+
namespace: 'serverlessAirline',
5+
serviceName: 'orders',
6+
});
7+
8+
export const handler = async (
9+
_event: unknown,
10+
_context: unknown
11+
): Promise<void> => {
12+
const metricTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago
13+
metrics.setTimestamp(metricTimestamp);
14+
metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
15+
};

Diff for: packages/metrics/src/Metrics.ts

+95-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Console } from 'node:console';
2-
import { Utility } from '@aws-lambda-powertools/commons';
2+
import { isDate } from 'node:util/types';
3+
import { Utility, isIntegerNumber } from '@aws-lambda-powertools/commons';
34
import type {
45
GenericLogger,
56
HandlerMethodDecorator,
@@ -9,6 +10,8 @@ import { EnvironmentVariablesService } from './config/EnvironmentVariablesServic
910
import {
1011
COLD_START_METRIC,
1112
DEFAULT_NAMESPACE,
13+
EMF_MAX_TIMESTAMP_FUTURE_AGE,
14+
EMF_MAX_TIMESTAMP_PAST_AGE,
1215
MAX_DIMENSION_COUNT,
1316
MAX_METRICS_SIZE,
1417
MAX_METRIC_VALUES_SIZE,
@@ -198,6 +201,11 @@ class Metrics extends Utility implements MetricsInterface {
198201
*/
199202
private storedMetrics: StoredMetrics = {};
200203

204+
/**
205+
* Custom timestamp for the metrics
206+
*/
207+
#timestamp?: number;
208+
201209
public constructor(options: MetricsOptions = {}) {
202210
super();
203211

@@ -571,6 +579,46 @@ class Metrics extends Utility implements MetricsInterface {
571579
this.clearMetadata();
572580
}
573581

582+
/**
583+
* Sets the timestamp for the metric.
584+
*
585+
* If an integer is provided, it is assumed to be the epoch time in milliseconds.
586+
* If a Date object is provided, it will be converted to epoch time in milliseconds.
587+
*
588+
* The timestamp must be a Date object or an integer representing an epoch time.
589+
* This should not exceed 14 days in the past or be more than 2 hours in the future.
590+
* Any metrics failing to meet this criteria will be skipped by Amazon CloudWatch.
591+
*
592+
* See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
593+
* See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatch-Logs-Monitoring-CloudWatch-Metrics.html
594+
*
595+
* @example
596+
* ```typescript
597+
* import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';
598+
*
599+
* const metrics = new Metrics({
600+
* namespace: 'serverlessAirline',
601+
* serviceName: 'orders',
602+
* });
603+
*
604+
* export const handler = async () => {
605+
* const metricTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago
606+
* metrics.setTimestamp(metricTimestamp);
607+
* metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
608+
* };
609+
* ```
610+
* @param timestamp - The timestamp to set, which can be a number or a Date object.
611+
*/
612+
public setTimestamp(timestamp: number | Date): void {
613+
if (!this.#validateEmfTimestamp(timestamp)) {
614+
this.#logger.warn(
615+
"This metric doesn't meet the requirements and will be skipped by Amazon CloudWatch. " +
616+
'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.'
617+
);
618+
}
619+
this.#timestamp = this.#convertTimestampToEmfFormat(timestamp);
620+
}
621+
574622
/**
575623
* Serialize the stored metrics into a JSON object compliant with the Amazon CloudWatch EMF (Embedded Metric Format) schema.
576624
*
@@ -627,7 +675,7 @@ class Metrics extends Utility implements MetricsInterface {
627675

628676
return {
629677
_aws: {
630-
Timestamp: new Date().getTime(),
678+
Timestamp: this.#timestamp ?? new Date().getTime(),
631679
CloudWatchMetrics: [
632680
{
633681
Namespace: this.namespace || DEFAULT_NAMESPACE,
@@ -940,6 +988,51 @@ class Metrics extends Utility implements MetricsInterface {
940988
}
941989
}
942990
}
991+
992+
/**
993+
* Validates a given timestamp based on CloudWatch Timestamp guidelines.
994+
*
995+
* Timestamp must meet CloudWatch requirements.
996+
* The time stamp can be up to two weeks in the past and up to two hours into the future.
997+
* See [Timestamps](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#about_timestamp)
998+
* for valid values.
999+
*
1000+
* @param timestamp - Date object or epoch time in milliseconds representing the timestamp to validate.
1001+
*/
1002+
#validateEmfTimestamp(timestamp: number | Date): boolean {
1003+
if (!isDate(timestamp) && !isIntegerNumber(timestamp)) {
1004+
return false;
1005+
}
1006+
1007+
const timestampMs = isDate(timestamp) ? timestamp.getTime() : timestamp;
1008+
const currentTime = new Date().getTime();
1009+
1010+
const minValidTimestamp = currentTime - EMF_MAX_TIMESTAMP_PAST_AGE;
1011+
const maxValidTimestamp = currentTime + EMF_MAX_TIMESTAMP_FUTURE_AGE;
1012+
1013+
return timestampMs >= minValidTimestamp && timestampMs <= maxValidTimestamp;
1014+
}
1015+
1016+
/**
1017+
* Converts a given timestamp to EMF compatible format.
1018+
*
1019+
* @param timestamp - The timestamp to convert, which can be either a number (in milliseconds) or a Date object.
1020+
* @returns The timestamp in milliseconds. If the input is invalid, returns 0.
1021+
*/
1022+
#convertTimestampToEmfFormat(timestamp: number | Date): number {
1023+
if (isIntegerNumber(timestamp)) {
1024+
return timestamp;
1025+
}
1026+
if (isDate(timestamp)) {
1027+
return timestamp.getTime();
1028+
}
1029+
/**
1030+
* If this point is reached, it indicates timestamp was neither a valid number nor Date
1031+
* Returning zero represents the initial date of epoch time,
1032+
* which will be skipped by Amazon CloudWatch.
1033+
**/
1034+
return 0;
1035+
}
9431036
}
9441037

9451038
export { Metrics };

Diff for: packages/metrics/src/constants.ts

+12
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ const MAX_METRIC_VALUES_SIZE = 100;
1818
* The maximum number of dimensions that can be added to a metric (0-indexed).
1919
*/
2020
const MAX_DIMENSION_COUNT = 29;
21+
/**
22+
* The maximum age of a timestamp in milliseconds that can be emitted in a metric.
23+
* This is set to 14 days.
24+
*/
25+
const EMF_MAX_TIMESTAMP_PAST_AGE = 14 * 24 * 60 * 60 * 1000;
26+
/**
27+
* The maximum age of a timestamp in milliseconds that can be emitted in a metric.
28+
* This is set to 2 hours.
29+
*/
30+
const EMF_MAX_TIMESTAMP_FUTURE_AGE = 2 * 60 * 60 * 1000;
2131

2232
/**
2333
* The unit of the metric.
@@ -73,4 +83,6 @@ export {
7383
MAX_DIMENSION_COUNT,
7484
MetricUnit,
7585
MetricResolution,
86+
EMF_MAX_TIMESTAMP_PAST_AGE,
87+
EMF_MAX_TIMESTAMP_FUTURE_AGE,
7688
};

Diff for: packages/metrics/src/types/Metrics.ts

+31
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,37 @@ interface MetricsInterface {
475475
* @param enabled - Whether to throw an error if no metrics are emitted
476476
*/
477477
setThrowOnEmptyMetrics(enabled: boolean): void;
478+
/**
479+
* Sets the timestamp for the metric.
480+
*
481+
* If an integer is provided, it is assumed to be the epoch time in milliseconds.
482+
* If a Date object is provided, it will be converted to epoch time in milliseconds.
483+
*
484+
* The timestamp must be a Date object or an integer representing an epoch time.
485+
* This should not exceed 14 days in the past or be more than 2 hours in the future.
486+
* Any metrics failing to meet this criteria will be skipped by Amazon CloudWatch.
487+
*
488+
* See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
489+
* See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatch-Logs-Monitoring-CloudWatch-Metrics.html
490+
*
491+
* @example
492+
* ```typescript
493+
* import { MetricUnit, Metrics } from '@aws-lambda-powertools/metrics';
494+
*
495+
* const metrics = new Metrics({
496+
* namespace: 'serverlessAirline',
497+
* serviceName: 'orders',
498+
* });
499+
*
500+
* export const handler = async () => {
501+
* const metricTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago
502+
* metrics.setTimestamp(metricTimestamp);
503+
* metrics.addMetric('successfulBooking', MetricUnit.Count, 1);
504+
* };
505+
* ```
506+
* @param timestamp - The timestamp to set, which can be a number or a Date object.
507+
*/
508+
setTimestamp(timestamp: number | Date): void;
478509
/**
479510
* Create a new Metrics instance configured to immediately flush a single metric.
480511
*

0 commit comments

Comments
 (0)