-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #27 from beforeyoubid/generic-metric-logger
Generic metric logger
- Loading branch information
Showing
10 changed files
with
341 additions
and
3 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,7 @@ | ||
**Example Metric on Mezmo** | ||
|
||
- On REST-API service, each time our partner sends a report pricing request to us, we produce the result to Mezmo | ||
- This usually means success or failure so we can track the performance of the overall pricing requests. | ||
- The current metric also support breakdown by partner id, type of error, success or failure, the acual slug if we would | ||
like to group them | ||
- ![Sample Metric](../docs/sample-metric.jpg) |
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,26 @@ | ||
import { ensureFlushAll, logger, MetricLogger } from '../src'; | ||
|
||
const yourHandler = async () => { | ||
// Normal string log message | ||
logger.info('some string goes here'); | ||
|
||
// 1) Define your custom metric logger object, you can define your own metric format | ||
const metric = { | ||
req: { type: 'myType', userId: 'my-user-id' }, | ||
res: { isSuccessful: false, errorCode: '', value: 10 }, | ||
}; | ||
|
||
// 2) Create a new instance of MetricLogger | ||
const metricLogger = new MetricLogger<typeof metric>(metric); | ||
|
||
// 3) Set any metric value to your metric object | ||
metricLogger.setMetric('res.value', 20); | ||
metricLogger.setMetric('res.isSucccessful', true); | ||
|
||
// 4) Send the metric to Mezmo, this will be logged as a JSON object | ||
// Should send once per execution to avoid duplicate metric | ||
metricLogger.sendMetric(); | ||
}; | ||
|
||
// Example if you use Metric Logger in your handler | ||
export default ensureFlushAll(yourHandler); |
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,26 @@ | ||
import { ensureFlushAll, logger, MetricLogger } from '../src'; | ||
|
||
const yourHandler = async () => { | ||
// Normal string log message | ||
logger.info('some string goes here'); | ||
|
||
// 1) Define your custom metric logger object, you can define your own metric format | ||
const metric = { | ||
type: 'email', | ||
status: 'send', | ||
emailType: 'receipt', | ||
}; | ||
|
||
// 2) Create a new instance of MetricLogger | ||
const metricLogger = new MetricLogger<typeof metric>(metric); | ||
|
||
// 3) Set any metric value to your metric object | ||
metricLogger.setMetric('status', 'ignore'); | ||
|
||
// 4) Send the metric to Mezmo, this will be logged as a JSON object | ||
// Should send once per execution to avoid duplicate metric | ||
metricLogger.sendMetric(); | ||
}; | ||
|
||
// Example if you use Metric Logger in your handler | ||
export default ensureFlushAll(yourHandler); |
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,154 @@ | ||
import { MetricLogger } from '../index'; | ||
import type { BasicMetric, BasicMetricReq, BasicMetricRes } from '../types'; | ||
import { logger } from '../../logger'; | ||
jest.mock('../../logger', () => ({ | ||
logger: { | ||
info: jest.fn(), | ||
}, | ||
})); | ||
|
||
type MyReqMetric = BasicMetricReq & { | ||
userId: string; | ||
}; | ||
|
||
type MyResMetric = BasicMetricRes & { | ||
count: number; | ||
}; | ||
|
||
type MyMetric = BasicMetric & { | ||
req: MyReqMetric; | ||
res: MyResMetric; | ||
}; | ||
|
||
type MyCustomMetric = { | ||
productType: string; | ||
count: number; | ||
}; | ||
|
||
describe('MetricLogger', () => { | ||
const defaultMetric = { | ||
req: { type: 'myType', userId: 'my-user-id' }, | ||
res: { isSuccessful: true, errorCode: '', count: 10 }, | ||
}; | ||
describe('setMetric() - extended metric type', () => { | ||
it('should set metric property', () => { | ||
const metricLogger = new MetricLogger<MyMetric>(defaultMetric, true, logger); | ||
|
||
const expectedUserId = 'another-user-id'; | ||
const expectedResult = { | ||
...defaultMetric, | ||
req: { ...defaultMetric.req, userId: expectedUserId }, | ||
}; | ||
metricLogger.setMetric('req.userId', expectedUserId); | ||
const result = metricLogger.getMetric(); | ||
expect(result.req.userId).toEqual(expectedUserId); | ||
expect(result).toEqual(expectedResult); | ||
}); | ||
|
||
it('should allow to set metrics', () => { | ||
const metricLogger = new MetricLogger<MyMetric>(defaultMetric, true, logger); | ||
|
||
const expectedUserId = 'another-user-id'; | ||
const newCount = 20; | ||
const expectedResult = { | ||
req: { ...defaultMetric.req, userId: expectedUserId }, | ||
res: { ...defaultMetric.res, count: newCount }, | ||
}; | ||
metricLogger.setMetric('req.userId', expectedUserId); | ||
metricLogger.setMetric('res.count', newCount); | ||
const result = metricLogger.getMetric(); | ||
expect(result.req.userId).toEqual(expectedUserId); | ||
expect(result).toEqual(expectedResult); | ||
}); | ||
|
||
it('should allow to set custom multi-layer metrics', () => { | ||
type MyMetricNestedLayer = MyMetric & { | ||
res: MyResMetric & { | ||
customObject: { | ||
someKey: string; | ||
someValue: string; | ||
}; | ||
}; | ||
}; | ||
const nestedDefaultMetric = { | ||
...defaultMetric, | ||
res: { | ||
...defaultMetric.res, | ||
customObject: { | ||
someKey: 'some-key', | ||
someValue: 'some-value', | ||
}, | ||
}, | ||
}; | ||
const metricLogger = new MetricLogger<MyMetricNestedLayer>(nestedDefaultMetric, true, logger); | ||
const expectedSomeKey = 'new-key'; | ||
const expectedSomeValue = 'new-value'; | ||
const expectedResult = { | ||
...nestedDefaultMetric, | ||
res: { | ||
...defaultMetric.res, | ||
customObject: { | ||
someKey: expectedSomeKey, | ||
someValue: expectedSomeValue, | ||
}, | ||
}, | ||
}; | ||
metricLogger.setMetric('res.customObject.someKey', expectedSomeKey); | ||
metricLogger.setMetric('res.customObject.someValue', expectedSomeValue); | ||
const result = metricLogger.getMetric(); | ||
expect(result).toEqual(expectedResult); | ||
}); | ||
}); | ||
|
||
describe('setMetric() - custom metric type', () => { | ||
it('should be able to handle custom metric', () => { | ||
const myDefaultMetric = { | ||
productType: 'myType', | ||
count: 10, | ||
}; | ||
const metricLogger = new MetricLogger<MyCustomMetric>(myDefaultMetric, true, logger); | ||
const expectedProductType = 'anotherType'; | ||
const expectedCount = 100; | ||
metricLogger.setMetric('productType', expectedProductType); | ||
metricLogger.setMetric('count', expectedCount); | ||
const result = metricLogger.getMetric(); | ||
expect(result.productType).toEqual(expectedProductType); | ||
expect(result.count).toEqual(expectedCount); | ||
}); | ||
}); | ||
|
||
describe('success()', () => { | ||
it('should set success response', () => { | ||
const metricLogger = new MetricLogger<MyMetric>(defaultMetric, true, logger); | ||
|
||
const expectedUserId = 'another-user-id'; | ||
const expectedResult = { | ||
...defaultMetric, | ||
req: { ...defaultMetric.req, userId: expectedUserId, isSuccessful: true, errorCode: '' }, | ||
}; | ||
metricLogger.setMetric('req.userId', expectedUserId); | ||
const result = metricLogger.getMetric(); | ||
expect(result.req.userId).toEqual(expectedUserId); | ||
expect(result.res.isSuccessful).toEqual(expectedResult.res.isSuccessful); | ||
expect(result.res.errorCode).toEqual(expectedResult.res.errorCode); | ||
}); | ||
}); | ||
|
||
describe('error()', () => { | ||
it('should set error response', () => { | ||
const metricLogger = new MetricLogger<MyMetric>(defaultMetric, true, logger); | ||
|
||
const expectedUserId = 'another-user-id'; | ||
const expectedResult = { | ||
req: { ...defaultMetric.req, userId: expectedUserId }, | ||
res: { ...defaultMetric.res, isSuccessful: false, errorCode: 'some-error-code' }, | ||
}; | ||
metricLogger.error('some-error-code'); | ||
metricLogger.setMetric('res.isSuccessful', false); | ||
const result = metricLogger.getMetric(); | ||
expect(result.req.userId).toEqual(expectedUserId); | ||
expect(result.res.isSuccessful).toEqual(expectedResult.res.isSuccessful); | ||
expect(result.res.errorCode).toEqual(expectedResult.res.errorCode); | ||
}); | ||
}); | ||
}); |
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,75 @@ | ||
import winston from 'winston'; | ||
import { logger } from '../logger'; | ||
|
||
export class MetricLogger<T> { | ||
private _logger: winston.Logger; | ||
private _metric: T; | ||
private _isTest: boolean; | ||
constructor(private readonly defaultMetric: T = {} as T, _isTest = false, suppliedLogger?: winston.Logger) { | ||
this._metric = defaultMetric; | ||
this._isTest = _isTest; | ||
this._logger = suppliedLogger || logger; | ||
} | ||
|
||
getMetric(): T { | ||
return this._metric; | ||
} | ||
|
||
/** | ||
* Trigger to send log message over the supplied logger object | ||
* @param metric | ||
* @param stringifyMetric | ||
*/ | ||
sendMetric(metric: T = this._metric, stringifyMetric = true): void { | ||
if (!this._isTest) { | ||
const metricToSend = stringifyMetric ? JSON.stringify(metric) : metric; | ||
this._logger.info(metricToSend); | ||
} | ||
} | ||
|
||
/** | ||
* Dynamically set the metric, support the dot notation e.g. `setMetric('res.isSuccess', true)` | ||
* @param key | ||
* @param value | ||
*/ | ||
setMetric(key: string, value: unknown) { | ||
const keys = key.split('.'); | ||
let parentObject = this._metric; | ||
for (let i = 0; i < keys.length; i++) { | ||
const leafNode = i === keys.length - 1; | ||
const objectExists = typeof parentObject[keys[i]] !== undefined; | ||
|
||
// If not a leaf node and the object does not exist, set parent object so we can keep traversing through themn | ||
if (objectExists && !leafNode) { | ||
parentObject = parentObject[keys[i]]; | ||
} | ||
|
||
if (leafNode) { | ||
parentObject[keys[i]] = value; | ||
break; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Capture the success | ||
* @param cachedResponse | ||
* @returns | ||
*/ | ||
success() { | ||
return this.sendMetric(this.getMetric()); | ||
} | ||
|
||
/** | ||
* A handy function to capture the error | ||
* @param errorCode | ||
* @param errorField | ||
* @returns | ||
*/ | ||
error(errorCode: string = '', errorField: string = 'res.errorCode') { | ||
if (errorField) { | ||
this.setMetric(errorField, errorCode); | ||
} | ||
return this.sendMetric(this.getMetric()); | ||
} | ||
} |
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,15 @@ | ||
export type BasicRecord = Record<string, unknown>; | ||
|
||
export type BasicMetricReq = BasicRecord & { | ||
type: string; | ||
}; | ||
|
||
export type BasicMetricRes = BasicRecord & { | ||
isSuccessful: boolean; | ||
errorCode: string; | ||
}; | ||
|
||
export type BasicMetric = { | ||
req: BasicMetricReq; | ||
res: BasicMetricRes; | ||
}; |
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