-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(createLambdaExtensionClient): Allow metrics propagation via Data…
…dog Lambda Extension (#196) * feat(createLambdaExtensionClient): Allow metrics propagation via Datadog Lambda Extension * Explicitly remove `decrement` from `createLambdaExtensionClient` * Cleanup and provide apt description. * Close up test coverage * Add README entry * Apply suggestions from code review Co-authored-by: Ryan Ling <[email protected]> * Drop README as it's autogenerated by GitHub * Export `createLambdaExtensionClient` * Move `@types/aws-lambda` into deps to fix TS type checking * Add migration notice and add test for `createLambdaExtensionClient` export * Fix README * Update package.json Co-authored-by: Ryan Ling <[email protected]> * Fix up `yarn.lock` --------- Co-authored-by: Ryan Ling <[email protected]>
- Loading branch information
Showing
10 changed files
with
355 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/** | ||
* Abstract interface for recording metrics for AWS Lambda | ||
*/ | ||
export interface LambdaExtensionMetricsClient { | ||
/** | ||
* Records a time in milliseconds | ||
* | ||
* @param name - Name of the metric to record. | ||
* @param value - Time in milliseconds. Fractional values are supported. | ||
* @param tags - Optional list of tags for the metric. | ||
*/ | ||
timing(name: string, value: number, tags?: string[]): void; | ||
|
||
/** | ||
* Measures the statistical distribution of a set of values | ||
* | ||
* @param name - Name of the metric to record. | ||
* @param value - Value to include in the statistical distribution. | ||
* @param tags - Optional list of tags for the metric. | ||
*/ | ||
distribution(name: string, value: number, tags?: string[]): void; | ||
|
||
/** | ||
* Increments a counter by one | ||
* | ||
* @param name - Name of the metric to increment. | ||
* @param tags - Optional list of tags for the metric. | ||
*/ | ||
increment(name: string, tags?: string[]): void; | ||
|
||
/** | ||
* Increments a counter by the specified integer count | ||
* | ||
* @param name - Name of the metric to increment. | ||
* @param count - Number to increment the counter by. | ||
* @param tags - Optional list of tags for the metric. | ||
*/ | ||
increment(name: string, count: number, tags?: string[]): void; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import * as datadogJS from 'datadog-lambda-js'; | ||
|
||
import { createLambdaExtensionClient } from './createLambdaExtensionClient'; | ||
|
||
const sendDistributionMetric = jest | ||
.spyOn(datadogJS, 'sendDistributionMetric') | ||
.mockReturnValue(); | ||
|
||
const datadog = jest.spyOn(datadogJS, 'datadog').mockReturnValue(() => {}); | ||
|
||
describe('createLambdaExtensionClient', () => { | ||
const { metricsClient, withLambdaExtension } = createLambdaExtensionClient({ | ||
name: 'test', | ||
metrics: true, | ||
}); | ||
|
||
const tags = ['pipe|special|char', 'env:prod']; | ||
|
||
afterEach(() => jest.resetAllMocks()); | ||
|
||
describe('withLambdaExtension', () => { | ||
it('should call the `datadog` wrapper', () => { | ||
withLambdaExtension(() => Promise.resolve({})); | ||
|
||
expect(datadog).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should not call the `datadog` wrapper when metrics is turned off', () => { | ||
const client = createLambdaExtensionClient({ | ||
name: 'test', | ||
metrics: false, | ||
}); | ||
|
||
client.withLambdaExtension(() => Promise.resolve({})); | ||
|
||
expect(datadog).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe('timing', () => { | ||
it('should record integer timings without tags', () => { | ||
metricsClient.timing('my_custom_metric', 100); | ||
|
||
expect(sendDistributionMetric).toHaveBeenCalledTimes(1); | ||
expect(sendDistributionMetric).toHaveBeenCalledWith( | ||
'test.my_custom_metric', | ||
100, | ||
); | ||
}); | ||
|
||
it('should record float timings with tags', () => { | ||
metricsClient.timing('my_custom_metric', 1234.5, tags); | ||
|
||
expect(sendDistributionMetric).toHaveBeenCalledTimes(1); | ||
expect(sendDistributionMetric).toHaveBeenCalledWith( | ||
'test.my_custom_metric', | ||
1234.5, | ||
'pipe_special_char', | ||
'env:prod', | ||
); | ||
}); | ||
}); | ||
|
||
describe('distribution', () => { | ||
it('should record integer values without tags', () => { | ||
metricsClient.distribution('my_custom_metric', 100); | ||
|
||
expect(sendDistributionMetric).toHaveBeenCalledTimes(1); | ||
expect(sendDistributionMetric).toHaveBeenCalledWith( | ||
'test.my_custom_metric', | ||
100, | ||
); | ||
}); | ||
|
||
it('should record float values with tags', () => { | ||
metricsClient.distribution('my_custom_metric', 1234.5, tags); | ||
|
||
expect(sendDistributionMetric).toHaveBeenCalledTimes(1); | ||
expect(sendDistributionMetric).toHaveBeenCalledWith( | ||
'test.my_custom_metric', | ||
1234.5, | ||
'pipe_special_char', | ||
'env:prod', | ||
); | ||
}); | ||
}); | ||
|
||
describe('increment', () => { | ||
it('should increment with an implicit value and no tags', () => { | ||
metricsClient.increment('my_custom_metric'); | ||
|
||
expect(sendDistributionMetric).toHaveBeenCalledTimes(1); | ||
expect(sendDistributionMetric).toHaveBeenCalledWith( | ||
'test.my_custom_metric', | ||
1, | ||
); | ||
}); | ||
|
||
it('should increment with an implicit value and tags', () => { | ||
metricsClient.increment('my_custom_metric', ['comma,special,char']); | ||
|
||
expect(sendDistributionMetric).toHaveBeenCalledTimes(1); | ||
expect(sendDistributionMetric).toHaveBeenCalledWith( | ||
'test.my_custom_metric', | ||
1, | ||
'comma_special_char', | ||
); | ||
}); | ||
|
||
it('should increment with an explicit value and no tags', () => { | ||
metricsClient.increment('my_custom_metric', 10); | ||
|
||
expect(sendDistributionMetric).toHaveBeenCalledTimes(1); | ||
expect(sendDistributionMetric).toHaveBeenCalledWith( | ||
'test.my_custom_metric', | ||
10, | ||
); | ||
}); | ||
|
||
it('should increment with an explicit value and tags', () => { | ||
metricsClient.increment('my_custom_metric', 10, ['compound:tag']); | ||
|
||
expect(sendDistributionMetric).toHaveBeenCalledTimes(1); | ||
expect(sendDistributionMetric).toHaveBeenCalledWith( | ||
'test.my_custom_metric', | ||
10, | ||
'compound:tag', | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import type { Context } from 'aws-lambda'; | ||
import { datadog, sendDistributionMetric } from 'datadog-lambda-js'; | ||
|
||
import { LambdaExtensionMetricsClient } from './LambdaExtensionMetricsClient'; | ||
|
||
interface DatadogMetric { | ||
name: string; | ||
tags?: string[]; | ||
value: number; | ||
} | ||
|
||
interface DatadogConfig { | ||
name: string; | ||
metrics: boolean; | ||
} | ||
|
||
type Handler<Event, Output> = ( | ||
event: Event, | ||
ctx: Readonly<Context>, | ||
) => Promise<Output>; | ||
|
||
interface LambdaExtensionClient { | ||
metricsClient: LambdaExtensionMetricsClient; | ||
/** | ||
* Conditionally wraps your AWS lambda handler function based on the provided config. | ||
* | ||
* This is necessary for initialising metrics/tracing support. | ||
*/ | ||
// This also "fixes" its broken type definitions. | ||
withLambdaExtension: <Event, Output = unknown>( | ||
fn: Handler<Event, Output>, | ||
) => Handler<Event, Output>; | ||
} | ||
|
||
/** | ||
* Replaces tag special characters with an underscore | ||
*/ | ||
const sanitiseTag = (tag: string): string => tag.replace(/\||@|,/g, '_'); | ||
|
||
/** | ||
* Creates a new Datadog Lambda client configured for the given app. | ||
* | ||
* @see {@link https://docs.datadoghq.com/serverless/libraries_integrations/extension/} | ||
* | ||
* @param config - Application configuration | ||
*/ | ||
export const createLambdaExtensionClient = ( | ||
config: DatadogConfig, | ||
): LambdaExtensionClient => { | ||
const send = (metric: DatadogMetric) => { | ||
const { value } = metric; | ||
|
||
const sanitisedLambdaName = config.name.replace(new RegExp('-', 'g'), '_'); | ||
const name = `${sanitisedLambdaName}.${metric.name.toLowerCase()}`; | ||
|
||
const tags = (metric.tags || []).map(sanitiseTag); | ||
|
||
if (config.metrics) { | ||
sendDistributionMetric(name, value, ...tags); | ||
} | ||
}; | ||
|
||
const sendCount = ( | ||
name: string, | ||
countOrTags?: number | string[], | ||
tagsIfCount?: string[], | ||
): void => { | ||
let count: number; | ||
let tags: string[] | undefined; | ||
|
||
if (typeof countOrTags === 'number') { | ||
count = countOrTags; | ||
tags = tagsIfCount; | ||
} else { | ||
count = 1; | ||
tags = countOrTags; | ||
} | ||
|
||
send({ name, tags, value: count }); | ||
}; | ||
|
||
return { | ||
withLambdaExtension: <Event, Output = unknown>( | ||
fn: Handler<Event, Output>, | ||
): Handler<Event, Output> => | ||
config.metrics ? (datadog(fn) as Handler<Event, Output>) : fn, | ||
|
||
metricsClient: { | ||
increment: sendCount, | ||
|
||
distribution: (name: string, value: number, tags?: string[]): void => { | ||
send({ name, tags, value }); | ||
}, | ||
|
||
timing: (name: string, value: number, tags?: string[]): void => { | ||
send({ name, tags, value }); | ||
}, | ||
}, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.