-
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.
feat(trail): expose withLogTrail and getResourceNameFromFileName
- Loading branch information
1 parent
24b187f
commit ca384aa
Showing
9 changed files
with
250 additions
and
53 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 was deleted.
Oops, something went wrong.
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,36 @@ | ||
/** | ||
* what: the shape of an observable procedure | ||
* | ||
* what^2: | ||
* - observable = easy to read, monitor, and maintain | ||
* - procedure = an executable of a tactic; tactic.how.procedure::Executable | ||
* | ||
* note | ||
* - javascript's "functions" are actually, by definition, procedures | ||
*/ | ||
export type Procedure = ( | ||
/** | ||
* the input of the procedure | ||
*/ | ||
input: any, | ||
|
||
/** | ||
* the context within which the procedure runs | ||
*/ | ||
context?: any, | ||
) => any; | ||
|
||
/** | ||
* extracts the input::Type of a procedure | ||
*/ | ||
export type ProcedureInput<T extends Procedure> = Parameters<T>[0]; | ||
|
||
/** | ||
* extracts the context::Type of a procedure | ||
*/ | ||
export type ProcedureContext<T extends Procedure> = Parameters<T>[1]; | ||
|
||
/** | ||
* extracts the output::Type of a procedure | ||
*/ | ||
export type ProcedureOutput<T extends Procedure> = ReturnType<T>; |
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 |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from './AsOfGlossary'; | ||
export * from './logic/withLogTrail'; | ||
export * from './logic/getResourceNameFromFileName'; |
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,3 @@ | ||
import { withLogTrail } from './withLogTrail'; | ||
|
||
export const asDomainProcedure = withLogTrail; // todo: also, ask for what, why, when, how.idea |
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,2 @@ | ||
export const getResourceNameFromFileName = (fileName: string): string => | ||
fileName.split('/').slice(-1)[0]!.split('.').slice(0, -1).join('.'); |
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,83 @@ | ||
import { withLogTrail } from './withLogTrail'; | ||
|
||
const logDebugSpy = jest.spyOn(console, 'log'); | ||
|
||
export const sleep = (ms: number) => | ||
new Promise((resolve) => setTimeout(resolve, ms)); | ||
|
||
describe('withLogTrail', () => { | ||
beforeEach(() => jest.clearAllMocks()); | ||
describe('input output logging', () => { | ||
it('should be log input and output for a sync fn', () => { | ||
const castToUpperCase = withLogTrail( | ||
({ name }: { name: string }) => { | ||
return name.toUpperCase(); | ||
}, | ||
{ name: 'castToUpperCase', log: { method: console.log } }, | ||
); | ||
|
||
// should run like normal | ||
const uppered = castToUpperCase({ name: 'casey' }); | ||
expect(uppered).toEqual('CASEY'); | ||
|
||
// should have logged input and output | ||
expect(logDebugSpy).toHaveBeenCalledTimes(2); | ||
expect(logDebugSpy).toHaveBeenNthCalledWith(1, 'castToUpperCase.input', { | ||
input: { name: 'casey' }, | ||
}); | ||
expect(logDebugSpy).toHaveBeenNthCalledWith(2, 'castToUpperCase.output', { | ||
input: { name: 'casey' }, | ||
output: ['CASEY'], | ||
}); | ||
}); | ||
it('should be log input and output for an async fn', async () => { | ||
const castToUpperCase = withLogTrail( | ||
async ({ name }: { name: string }) => { | ||
await sleep(100); | ||
return name.toUpperCase(); | ||
}, | ||
{ name: 'castToUpperCase', log: { method: console.log } }, | ||
); | ||
|
||
// should run like normal | ||
const uppered = await castToUpperCase({ name: 'casey' }); | ||
expect(uppered).toEqual('CASEY'); | ||
|
||
// should have logged input and output | ||
expect(logDebugSpy).toHaveBeenCalledTimes(2); | ||
expect(logDebugSpy).toHaveBeenNthCalledWith(1, 'castToUpperCase.input', { | ||
input: { name: 'casey' }, | ||
}); | ||
expect(logDebugSpy).toHaveBeenNthCalledWith(2, 'castToUpperCase.output', { | ||
input: { name: 'casey' }, | ||
output: ['CASEY'], | ||
}); | ||
}); | ||
}); | ||
describe('duration reporting', () => { | ||
it('should report the duration of an operation if it takes more than 1 second by default', async () => { | ||
const castToUpperCase = withLogTrail( | ||
async ({ name }: { name: string }) => { | ||
await sleep(1100); | ||
return name.toUpperCase(); | ||
}, | ||
{ name: 'castToUpperCase', log: { method: console.log } }, | ||
); | ||
|
||
// should run like normal | ||
const uppered = await castToUpperCase({ name: 'casey' }); | ||
expect(uppered).toEqual('CASEY'); | ||
|
||
// should have logged input and output | ||
expect(logDebugSpy).toHaveBeenCalledTimes(2); | ||
expect(logDebugSpy).toHaveBeenNthCalledWith(1, 'castToUpperCase.input', { | ||
input: { name: 'casey' }, | ||
}); | ||
expect(logDebugSpy).toHaveBeenNthCalledWith(2, 'castToUpperCase.output', { | ||
input: { name: 'casey' }, | ||
output: ['CASEY'], | ||
duration: expect.stringContaining(' sec'), | ||
}); | ||
}); | ||
}); | ||
}); |
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,117 @@ | ||
import { UnexpectedCodePathError } from '@ehmpathy/error-fns'; | ||
import { LogMethod } from 'simple-leveled-log-methods'; | ||
import { isAPromise } from 'type-fns'; | ||
|
||
const noOp = (...input: any) => input; | ||
const omitContext = (...input: any) => input[0]; // standard pattern for args = [input, context] | ||
const roundToHundredths = (num: number) => Math.round(num * 100) / 100; // https://stackoverflow.com/a/14968691/3068233 | ||
|
||
/** | ||
* enables input output logging and tracing for a method | ||
* | ||
* todo: - add tracing identifier w/ async-context | ||
* todo: - hookup visual tracing w/ external lib (vi...lo...) | ||
* todo: - bundle this with its own logging library which supports scoped logs | ||
*/ | ||
export const withLogTrail = <T extends (...args: any[]) => any>( | ||
logic: T, | ||
{ | ||
name: declaredName, | ||
durationReportingThresholdInSeconds = 1, | ||
log: logInput, | ||
}: { | ||
/** | ||
* specifies the name of the function, if the function does not have a name assigned already | ||
*/ | ||
name?: string; | ||
|
||
/** | ||
* enable redacting parts of the input or output from logging | ||
*/ | ||
log: | ||
| LogMethod | ||
| { | ||
/** | ||
* specifies the log method to use to log with | ||
*/ | ||
method: LogMethod; // TODO: use a logger which leverages async-context to scope all logs created inside of this fn w/ `${name}.progress: ${message}`; at that point, probably stick "inout output tracing" inside of that lib | ||
|
||
/** | ||
* what of the input to log | ||
*/ | ||
input?: (...value: Parameters<T>) => any; | ||
|
||
/** | ||
* what of the output to log | ||
*/ | ||
output?: (value: Awaited<ReturnType<T>>) => any; | ||
}; | ||
|
||
/** | ||
* specifies the threshold after which a duration will be included on the output log | ||
*/ | ||
durationReportingThresholdInSeconds?: number; | ||
}, | ||
) => { | ||
// cache the name of the function per wrapping | ||
const name: string | null = logic.name || declaredName || null; // use `\\` since `logic.name` returns `""` for anonymous functions | ||
|
||
// if no name is identifiable, throw an error here to fail fast | ||
if (!name) | ||
throw new UnexpectedCodePathError( | ||
'could not identify name for wrapped function', | ||
); | ||
|
||
// if the name specified does not match the name of the function, throw an error here to fail fast | ||
if (declaredName && name !== declaredName) | ||
throw new UnexpectedCodePathError( | ||
'the natural name of the function is different than the declared name', | ||
{ declaredName, naturalName: name }, | ||
); | ||
|
||
// extract the log methods | ||
const logMethod: LogMethod = | ||
'method' in logInput ? logInput.method : logInput; | ||
const logInputMethod = | ||
('input' in logInput ? logInput.input : undefined) ?? omitContext; | ||
const logOutputMethod = | ||
('output' in logInput ? logInput.output : undefined) ?? noOp; | ||
|
||
// wrap the function | ||
return ((...input: any): any => { | ||
// now log the input | ||
logMethod(`${name}.input`, { input: logInputMethod(...input) }); | ||
|
||
// begin tracking duration | ||
const startTimeInMilliseconds = new Date().getTime(); | ||
|
||
// now execute the method | ||
const result = logic(...input); | ||
|
||
// define what to do when we have output | ||
const logOutput = (output: Awaited<ReturnType<T>>) => { | ||
const endTimeInMilliseconds = new Date().getTime(); | ||
const durationInMilliseconds = | ||
endTimeInMilliseconds - startTimeInMilliseconds; | ||
const durationInSeconds = roundToHundredths(durationInMilliseconds / 1e3); // https://stackoverflow.com/a/53970656/3068233 | ||
logMethod(`${name}.output`, { | ||
input: logInputMethod(...input), | ||
output: logOutputMethod(output), | ||
...(durationInSeconds >= durationReportingThresholdInSeconds | ||
? { duration: `${durationInSeconds} sec` } // only include the duration if the threshold was crossed | ||
: {}), | ||
}); | ||
}; | ||
|
||
// if result is a promise, ensure we log after the output resolves | ||
if (isAPromise(result)) | ||
return result.then((output) => { | ||
logOutput(output); | ||
return output; | ||
}); | ||
|
||
// otherwise, its not a promise, so its done, so log now and return the result | ||
logOutput(result); | ||
return result; | ||
}) as T; | ||
}; |