Skip to content

Commit 2ec8a84

Browse files
authored
Allow setting timestamp of metrics. (#69 by @Grundlefleck)
Allow setting timestamp of metrics.
2 parents 65927e5 + 62f2559 commit 2ec8a84

File tree

4 files changed

+132
-4
lines changed

4 files changed

+132
-4
lines changed

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,29 @@ Requirements:
187187

188188
Examples:
189189

190-
```py
190+
```js
191191
setNamespace("MyApplication");
192192
```
193193

194+
- **setTimestamp**(Date | number timestamp)
195+
196+
Sets the CloudWatch [timestamp](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#about_timestamp) that extracted metrics are associated with. If not set a default value of `new Date()` will be used.
197+
198+
If set for a given `MetricsLogger`, timestamp will be preserved across calls to flush().
199+
200+
Requirements:
201+
* Date or Unix epoch millis, up to two weeks in the past and up to two hours in the future, as enforced by [CloudWatch](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#about_timestamp).
202+
203+
Examples:
204+
205+
```js
206+
setTimestamp(new Date())
207+
setTimestamp(new Date().getTime())
208+
```
209+
194210
- **flush**()
195211

196-
Flushes the current MetricsContext to the configured sink and resets all properties, dimensions and metric values. The namespace and default dimensions will be preserved across flushes.
212+
Flushes the current MetricsContext to the configured sink and resets all properties, dimensions and metric values. The namespace and default dimensions will be preserved across flushes. Timestamp will be preserved if set explicitly via `setTimestamp()`.
197213

198214
## Configuration
199215

src/logger/MetricsContext.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export class MetricsContext {
3939
private dimensions: Array<Record<string, string>>;
4040
private defaultDimensions: Record<string, string>;
4141
private shouldUseDefaultDimensions = true;
42+
private timestamp: Date | number | undefined;
4243

4344
/**
4445
* Constructor used to create child instances.
@@ -56,14 +57,26 @@ export class MetricsContext {
5657
properties?: IProperties,
5758
dimensions?: Array<Record<string, string>>,
5859
defaultDimensions?: Record<string, string>,
60+
timestamp?: Date | number,
5961
) {
6062
this.namespace = namespace || Configuration.namespace
6163
this.properties = properties || {};
6264
this.dimensions = dimensions || [];
63-
this.meta.Timestamp = new Date().getTime();
65+
this.timestamp = timestamp;
66+
this.meta.Timestamp = MetricsContext.resolveMetaTimestamp(timestamp);
6467
this.defaultDimensions = defaultDimensions || {};
6568
}
6669

70+
private static resolveMetaTimestamp(timestamp?: Date | number): number {
71+
if (timestamp instanceof Date) {
72+
return timestamp.getTime()
73+
} else if (timestamp) {
74+
return timestamp;
75+
} else {
76+
return new Date().getTime();
77+
}
78+
}
79+
6780
public setNamespace(value: string): void {
6881
this.namespace = value;
6982
}
@@ -72,6 +85,11 @@ export class MetricsContext {
7285
this.properties[key] = value;
7386
}
7487

88+
public setTimestamp(timestamp: Date | number) {
89+
this.timestamp = timestamp;
90+
this.meta.Timestamp = MetricsContext.resolveMetaTimestamp(timestamp);
91+
}
92+
7593
/**
7694
* Sets default dimensions for the Context.
7795
* A dimension set will be created with just the default dimensions
@@ -173,6 +191,7 @@ export class MetricsContext {
173191
Object.assign({}, this.properties),
174192
Object.assign([], this.dimensions),
175193
this.defaultDimensions,
194+
this.timestamp
176195
);
177196
}
178197
}

src/logger/MetricsLogger.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,21 @@ export class MetricsLogger {
114114
return this;
115115
}
116116

117+
/**
118+
* Set the timestamp of metrics emitted in this context.
119+
*
120+
* If not set, the timestamp will default to new Date() at the point
121+
* the context is constructed.
122+
*
123+
* If set, timestamp will preserved across calls to flush().
124+
*
125+
* @param timestamp
126+
*/
127+
public setTimestamp(timestamp: Date | number): MetricsLogger {
128+
this.context.setTimestamp(timestamp);
129+
return this;
130+
}
131+
117132
/**
118133
* Creates a new logger using the same contextual data as
119134
* the previous logger. This allows you to flush the instances

src/logger/__tests__/MetricsLogger.test.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,76 @@ test('can set namespace', async () => {
199199
expect(actualValue).toBe(expectedValue);
200200
});
201201

202+
test('defaults timestamp to now', async () => {
203+
// arrange
204+
const before = new Date();
205+
// recreate logger to regenerate meta.Timestamp set to now
206+
sink = createSink();
207+
environment = createEnvironment(sink);
208+
logger = createLogger(() => Promise.resolve(environment));
209+
210+
// act
211+
logger.putMetric(faker.random.word(), faker.random.number());
212+
await logger.flush();
213+
214+
//assert
215+
const after = new Date();
216+
const lastEvent = sink.events.slice(-1)[0];
217+
expectTimestampWithin(lastEvent, [before, after]);
218+
});
219+
220+
test('can set timestamp', async () => {
221+
// arrange
222+
const timestamp = faker.date.recent();
223+
logger.setTimestamp(timestamp)
224+
225+
// act
226+
logger.putMetric(faker.random.word(), faker.random.number());
227+
await logger.flush();
228+
229+
//assert
230+
expect(sink.events.length).toEqual(1);
231+
expect(sink.events[0].meta.Timestamp).toEqual(timestamp.getTime());
232+
});
233+
234+
test('flush() preserves timestamp if set explicitly', async () => {
235+
// arrange
236+
const timestamp = faker.date.recent();
237+
logger.setTimestamp(timestamp)
238+
239+
// act
240+
logger.putMetric(faker.random.word(), faker.random.number());
241+
await logger.flush();
242+
logger.putMetric(faker.random.word(), faker.random.number());
243+
await logger.flush();
244+
245+
//assert
246+
expect(sink.events.length).toEqual(2);
247+
expect(sink.events[1].meta.Timestamp).toEqual(timestamp.getTime());
248+
});
249+
250+
test('flush() resets timestamp to now if not set explicitly', async () => {
251+
// arrange
252+
const before = new Date();
253+
// recreate logger to regenerate meta.Timestamp set to now
254+
sink = createSink();
255+
environment = createEnvironment(sink);
256+
logger = createLogger(() => Promise.resolve(environment));
257+
// act
258+
logger.putMetric(faker.random.word(), faker.random.number());
259+
await logger.flush();
260+
const afterFirstFlush = new Date();
261+
logger.putMetric(faker.random.word(), faker.random.number());
262+
await logger.flush();
263+
const afterSecondFlush = new Date();
264+
265+
//assert
266+
expect(sink.events.length).toEqual(2);
267+
268+
expectTimestampWithin(sink.events[0], [before, afterFirstFlush]);
269+
expectTimestampWithin(sink.events[1], [afterFirstFlush, afterSecondFlush]);
270+
});
271+
202272
test('flush() uses configured serviceName for default dimension if provided', async () => {
203273
// arrange
204274
const expected = faker.random.word();
@@ -268,13 +338,15 @@ test('context is preserved across flush() calls', async () => {
268338
const expectedDimensionKey = 'Dim';
269339
const expectedPropertyKey = 'Prop';
270340
const expectedValues = 'Value';
341+
const expectedTimestamp = faker.date.recent();
271342

272343
const dimensions: Record<string, string> = {};
273344
dimensions[expectedDimensionKey] = expectedValues;
274345

275346
logger.setNamespace(expectedNamespace);
276347
logger.setProperty(expectedPropertyKey, expectedValues);
277348
logger.setDimensions(dimensions);
349+
logger.setTimestamp(expectedTimestamp);
278350

279351
// act
280352
logger.putMetric(metricKey, 0);
@@ -287,10 +359,11 @@ test('context is preserved across flush() calls', async () => {
287359
expect(sink.events).toHaveLength(2);
288360
for (let i = 0; i < sink.events.length; i++) {
289361
const evt = sink.events[i];
290-
// namespace, properties, dimensions should survive flushes
362+
// namespace, properties, dimensions, timestamp should survive flushes
291363
expect(evt.namespace).toBe(expectedNamespace);
292364
expect(evt.getDimensions()[0][expectedDimensionKey]).toBe(expectedValues);
293365
expect(evt.properties[expectedPropertyKey]).toBe(expectedValues);
366+
expect(evt.meta.Timestamp).toEqual(expectedTimestamp.getTime());
294367
// metric values should not survive flushes
295368
// @ts-ignore
296369
expect(evt.metrics.get(metricKey).values).toStrictEqual([i]);
@@ -306,3 +379,8 @@ const expectDimension = (key: string, value: string) => {
306379
const actualValue = dimension[key];
307380
expect(actualValue).toBe(value);
308381
};
382+
383+
const expectTimestampWithin = (context: MetricsContext, range: [Date, Date]) => {
384+
expect(context.meta.Timestamp).toBeGreaterThanOrEqual(range[0].getTime());
385+
expect(context.meta.Timestamp).toBeLessThanOrEqual(range[1].getTime());
386+
}

0 commit comments

Comments
 (0)