wkr-util
Version:
Utility library for wkr project.
214 lines (178 loc) • 8.35 kB
JavaScript
import fetch from 'node-fetch'
import urlJoin from 'url-join'
import {compose, curry, ifElse, always, identity, filter, pick, get, isObject, liftA} from '@cullylarson/f'
import {responseData, formatIpv4} from './index'
export const logLevels = {
TRACE: 'TRACE',
DEBUG: 'DEBUG',
INFO: 'INFO',
NOTICE: 'NOTICE',
WARN: 'WARN',
ERROR: 'ERROR',
ALERT: 'ALERT',
FATAL: 'FATAL',
}
export const errorToObj = err => {
if(!isObject(err)) return err
const elasticSearchError = get(['meta', 'body', 'error', 'root_cause'], [], err)
.map(get('reason', null))
.filter(Boolean)
.join(' | ')
const errMessage = get('message', undefined, err)
const finalMessage = [
errMessage || null,
elasticSearchError || null,
]
.filter(Boolean)
.join(': ')
return {
name: get('name', undefined, err),
errno: get('errno', undefined, err),
code: get('code', undefined, err),
message: finalMessage || undefined,
stack: get('stack', undefined, err),
}
}
export const reqToObj = pick(['baseUrl', 'body', 'fresh', 'hostname', 'ip', 'ips', 'method', 'originalUrl', 'params', 'path', 'protocol', 'query', 'secure', 'xhr'])
export const reqToIp = compose(formatIpv4, get('ip', undefined))
const getHeader = curry((headerName, expressRequestInfo, defaultValue) => {
if(!expressRequestInfo) return defaultValue
expressRequestInfo = liftA(expressRequestInfo)
const [req] = expressRequestInfo
return req && req.header(headerName) ? req.header(headerName) : defaultValue
})
export const getReqTraceId = getHeader('x-wkr-trace-id')
export const getReqTraceAppId = getHeader('x-wkr-trace-app-id')
export const getReqTraceSessionId = getHeader('x-wkr-trace-sess-id')
const buildLogData = (source, label, level, message, traceId, traceAppId, traceSessionId, extraFields, expressRequestInfo = undefined, exception = undefined) => {
const getObjFromRequest = (expressRequestInfo) => {
if(!expressRequestInfo) return {}
expressRequestInfo = liftA(expressRequestInfo)
const [request, blacklist] = expressRequestInfo
return compose(
ifElse(
always(blacklist),
filter((v, k) => {
// make sure the key isn't in the blacklist
return Array.isArray(blacklist) ? !blacklist.includes(k) : blacklist !== k
}),
identity,
),
reqToObj,
)(request)
}
return {
// set some data from the request
...(expressRequestInfo ? {
request: getObjFromRequest(expressRequestInfo),
ip: reqToIp(Array.isArray(expressRequestInfo) ? get(0, undefined, expressRequestInfo) : expressRequestInfo),
} : {}),
// set some data from the exception
...(exception ? {
exception: errorToObj(exception),
} : {}),
...extraFields,
source,
label,
level,
traceId,
traceAppId,
traceSessionId,
message,
}
}
// build the 'add' function. this is just here so that all add functions will have the same signature. they all use addFull anyway, so no need to redefine the signature every time.
const buildAdd = addFull => (label, level, message, extraFields, expressRequestInfo = undefined, exception = undefined) => addFull(
label,
level,
message,
{
traceId: getReqTraceId(expressRequestInfo, undefined),
traceAppId: getReqTraceAppId(expressRequestInfo, undefined),
traceSessionId: getReqTraceSessionId(expressRequestInfo, undefined),
},
extraFields,
expressRequestInfo,
exception,
)
export const LogRepo = (appAuth, source, apiUrl) => {
// traceId is a random string that is hopefully unique to each request. It should be passed from one service to another, if there are multiple services involved in a single request (e.g. A originates a request to B, B calls service C and passed traceId to it, C calls D and passes traceId again).
// traceAppId is an identifier for the app originating the request (e.g. A in the example above).
// traceSessionId is an identifier for the user (a user id or a random string for visitors). It should preferrably remain the same for the users whole existence. It allows tracing logs over the course of a users entire use session.
//
// expressRequestInfo can be a request or an array of [request, blacklist] where blacklist is an array of keys that shouldn't
// be included from the request
const addFull = (label, level, message, { traceId, traceAppId, traceSessionId }, extraFields, expressRequestInfo = undefined, exception = undefined) => {
const addUrl = urlJoin(apiUrl, '/api/v1/log/add')
const data = buildLogData(source, label, level, message, traceId, traceAppId, traceSessionId, extraFields, expressRequestInfo, exception)
return appAuth.getToken()
.then(token => fetch(addUrl, {
method: 'post',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
}))
.then(responseData)
.then(({response, data}) => {
if(!response.ok) {
const now = new Date()
if(data.error) {
const errors = liftA(data.error)
.join('\n')
console.error(`Could not log event (Stamp: ${now.getTime()} -- ${now.toString()}) (Label: ${label}, Level: ${level}, Message: ${message}). Got response status: ${response.status}. Response error message: ${errors}`)
}
else {
console.error(`Could not log event (Stamp: ${now.getTime()} -- ${now.toString()}) (Label: ${label}, Level: ${level}, Message: ${message}). Got response status: ${response.status}`)
}
}
})
.catch(err => {
const now = new Date()
console.error(`Exception while logging event (Stamp: ${now.getTime()} -- ${now.toString()}) (Label: ${label}, Level: ${level}, Message: ${message}). Got exception: [${err.name || ''} -- ${err.message || ''}]. Stack: ${err.stack || ''}`)
})
}
// A shorter version of addFull. See addFull for param details. Fields are pulled from the request, if possible. This is such a common use-case, it's easier to have a shorter function call to pull request fields.
const add = buildAdd(addFull)
return {
addFull,
add,
}
}
// doesn't do anything. used if you want to disable logs.
export const LogRepoNoop = () => ({
add: () => Promise.resolve(undefined),
addFull: () => Promise.resolve(undefined),
})
// Logs errors to console
export const LogRepoConsole = () => {
const addFull = (label, level, message, { traceId, traceAppId, traceSessionId }, extraFields, expressRequestInfo = undefined, exception = undefined) => {
const now = new Date()
const data = {
stamp: now.getTime(),
stampHuman: now.toString(),
...buildLogData('', label, level, message, traceId, traceAppId, traceSessionId, extraFields, expressRequestInfo, exception),
}
console.log(data)
if(data.exception && data.exception.stack) console.log('PRETTY STACK: ', data.exception.stack)
return Promise.resolve(undefined)
}
const add = buildAdd(addFull)
return {
addFull,
add,
}
}
// instead of sending logs to an API, will just pass them to a function. callback should return a promise.
export const LogRepoPassthru = (source, callback) => {
const addFull = (label, level, message, { traceId, traceAppId, traceSessionId }, extraFields, expressRequestInfo = undefined, exception = undefined) => {
const data = buildLogData(source, label, level, message, traceId, traceAppId, traceSessionId, extraFields, expressRequestInfo, exception)
return callback(data)
}
const add = buildAdd(addFull)
return {
addFull,
add,
}
}