Skip to content

Commit

Permalink
Merge pull request #27 from beforeyoubid/generic-metric-logger
Browse files Browse the repository at this point in the history
Generic metric logger
  • Loading branch information
chainat-byb authored Nov 26, 2024
2 parents 0166476 + 1626331 commit 1191a0e
Show file tree
Hide file tree
Showing 10 changed files with 341 additions and 3 deletions.
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ can also helps you out as well.
- Also moved `createLoggerObject()` into this shared module so it's easier to use and prefix message before returning
the actual logger object. e.g. `createLoggerObject(`[LOG-PREFIX]`);`
- ![V2 Diagram](./docs/v2-diagram.png)
- **v2.0.3 onwards**
- Adding generic `MetricLogger`

## 1) Use Console Logger

Expand Down Expand Up @@ -165,11 +167,34 @@ module so they look like this. ![Log prefix](./docs/log-prefix.png)
```
import { getLoggerObject } from '@beforeyoubid/logger-adapter';
const logger = getLoggerObject('[CoreLogicListingProvider]');
const logger = getLoggerObject('[Your Custom Prefix]');
logger.info('the usual log string...')
```

**Notes that** this function just wraps the original Winston logger instance your normally access from this line.
`import { logger } from '@beforeyoubid/logger-adapter';`. It only wraps and add the prefix the log line for you and
returns with debug, info, warn, error functions.

## MetricLogger

**Motivation:**

- Mezmo is one of the leading telemetry tools for logging and debugging
- We can also utilise Mezmo to collect, process and display business metrics based on log messages without extra fee on
the existing plan
- The setup is very simple and Mezmo does this out of the box

**How does it work?**

- When a JSON string (e.g. `JSON.stringify(jsonObject)`) is sent to Mezmo, Mezmo is smart enough to parse this into a
json object and index them automatically
- With this process, we can search for any value in the object and/or create a dashboard on any specific metric we see
fit
- The only drawback, the data retention is usually limited to 30 days as per the data retention policy on the account

**How to use:**

- [Sample Metric](./samples/metricLogger.md)
- [Sample 1](./samples/metricLogger1.sample.ts)
- [Sample 2](./samples/metricLogger2.sample.ts)
Binary file added docs/sample-metric.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@beforeyoubid/logger-adapter",
"version": "2.0.2",
"version": "2.0.4",
"description": "A platform logger module to send the log messages to Mezmo (formerly LogDNA).",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
7 changes: 7 additions & 0 deletions samples/metricLogger.md
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)
26 changes: 26 additions & 0 deletions samples/metricLogger1.sample.ts
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);
26 changes: 26 additions & 0 deletions samples/metricLogger2.sample.ts
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);
154 changes: 154 additions & 0 deletions src/MetricLogger/__tests__/index.spec.ts
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);
});
});
});
75 changes: 75 additions & 0 deletions src/MetricLogger/index.ts
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());
}
}
15 changes: 15 additions & 0 deletions src/MetricLogger/types.ts
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;
};
12 changes: 11 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import { logger } from './logger';
import { getLoggerObject } from './logger/util';
import { getLogParams } from './params';
import { flushAll, ensureFlushAll, ensureFlushAllCallback } from './util';
import { MetricLogger } from './MetricLogger';

// Export all the functions
export { consoleLogger, logger, getLoggerObject, getLogParams, flushAll, ensureFlushAll, ensureFlushAllCallback };
export {
consoleLogger,
logger,
getLoggerObject,
getLogParams,
flushAll,
ensureFlushAll,
ensureFlushAllCallback,
MetricLogger,
};

0 comments on commit 1191a0e

Please sign in to comment.