@infotorg/winston-log-formats
Version:
Infotorg log formats for Winston logger
159 lines • 11.7 kB
JavaScript
;const{format}=require("logform");const{LEVEL,MESSAGE}=require("triple-beam");const{isObject}=require("./utils/is-object");const isAxiosError=info=>{return info.isAxiosError===true||// For Axios ^0.25.0
info.name==="AxiosError"// For Axios ^1.3.3
};const isAxiosRequest=info=>{if(typeof info.message!=="object"){return false}const{message}=info;if(message!==null&&message!==void 0&&message.isAxiosRequest){// First attempt to identify Axios request object.
// isAxiosRequest is a custom property that is added to Axios request object in the request interceptor,
// and is used to identify Axios request.
return true}// Second attempt to identify Axios request object.
const expectedFieldsInConfig=["method","url"];const configFields=new Set(Object.keys(message));return expectedFieldsInConfig.filter(field=>configFields.has(field)).length===2};const isAxiosResponse=info=>{var _info$message,_info$message2,_info$message2$config,_info$message2$config2;if(typeof info.message!=="object"){return false}if((_info$message=info.message)!==null&&_info$message!==void 0&&_info$message.isAxiosResponse){// First attempt to identify Axios response object.
// isAxiosResponse is a custom property that is added to Axios response object in the response interceptor,
// and is used to identify Axios response.
return true}// Second attempt to identify Axios response object.
return(_info$message2=info.message)===null||_info$message2===void 0?void 0:(_info$message2$config=_info$message2.config)===null||_info$message2$config===void 0?void 0:(_info$message2$config2=_info$message2$config.headers["User-Agent"])===null||_info$message2$config2===void 0?void 0:_info$message2$config2.includes("axios")};/**
* Applied to errors like ECONNREFUSED, ECONNRESET, ENOTFOUND, etc.
*
* @type {string[]}
*/const errorFields=["errno","syscall","hostname","code","address","port"];const getWhiteListFields=options=>["url","baseURL","method","data",options!==null&&options!==void 0&&options.headers?"headers":null,"status","statusText",// Exists for ECONNREFUSED, ECONNRESET, ENOTFOUND, etc.
...errorFields,// ^^^^^^^^
"config","xsrfCookieName","xsrfHeaderName","xTrackId","timeout",options!==null&&options!==void 0&&options.stack?"stack":null,"responseTime",// Custom field provided by axios interceptors
"requestStartedAt"].filter(Boolean);/**
* Returns an object with all non axios info properties.
* They could be provided from other formats like timestamp, request-id, etc.
*
* @param {Object} info
* @returns {Object}
*/const topLevelInfo=info=>{// These keys are exist in Axios Error object
// Do not include these keys from top level of Axios Error object
const keys=["config","request","response","isAxiosError","toJSON",...errorFields];return Object.fromEntries(Object.entries(info).filter(([key])=>keys.includes(key)===false))};/**
* Extracts meta information from Axios request, response or error object
*
* @param {Object} data - Axios request, response or error object
* @param {string} reqOrResKey - 'req' or 'res'
* @param {[]} whiteListFields - array of fields to extract
* @returns {{req: {}, res: {} }}
*/const extractMeta=(data,reqOrResKey,whiteListFields)=>{const result={[reqOrResKey]:{}};Object.keys(data).filter(propName=>whiteListFields.includes(propName)).forEach(propName=>{if(propName==="config"){// The only axios Response and Error objects have 'config' key. Request object itself is a config.
// In Response and Error objects 'headers' key located under the 'config' key
// Remove headers from config also if not specified in the options
result.req=extractMeta(data[propName],"req",whiteListFields).req}else{result[reqOrResKey][propName]=data[propName]}});return result};/**
* Default axios format options.
*
* @type {Readonly<{headers: boolean, stack: boolean, errorDescription: string, responseDescription: string, meta: boolean, metaKey: string, requestDescription: string, enabled: boolean}>}
*/const defaultOptions=Object.freeze({enabled:true,// Description for a request
requestDescription:"Axios request",// Description for a response
responseDescription:"Axios response",// Description for an error
errorDescription:"Axios error",// If true, then request/response/error information will be included in meta
meta:false,// Key name for meta property
metaKey:"meta",// If true, then headers will be included in the meta
headers:true,// If true, then error stack trace will be included in the meta
stack:false});/**
* function axios (info)
* Returns a new instance of the axios Format which transforms `message` property if this is an axios request/response/error
* and adds this information into `metaKey` property in the message.
*
* If the `message` property of the `info` object is an instance of `AxiosError`,
* replace the `Error` object its own `message` property and meta information.
*
* @param {Object|Error} [axiosInfo={}] The info parameter provided to a given format represents a single log message. The object itself is mutable. Every info must have at least the level and message properties.
* @param {string|{}} axiosInfo.message Error message or request/response object. Required
* @param {string} axiosInfo.level Log level. Required
* @param {string|undefined} [axiosInfo.label=undefined] Log label. Optional
*
* @param {Object} [opts={meta: false, metaKey: 'meta', requestDescription: 'Axios request', responseDescription: 'Axios response', errorDescription: 'Axios error', stack: false, headers: true}] Setting specific options to the current instance of the format. Example: { headers: true, meta: true, metaKey: 'meta', requestDescription: 'Axios request', responseDescription: 'Axios response', errorDescription: 'Axios error', stack: true }
*
* @param {string} [opts.enabled=true] Toggle axios format output.
* @param {string} [opts.requestDescription='Axios request'] Description for a Request.
* @param {string} [opts.responseDescription='Axios response'] Description for a Response.
* @param {string} [opts.errorDescription='Axios error'] Description for an Error.
* @param {boolean} [opts.meta=false] If true, then request/response/error information will be included in meta.
* @param {string} [opts.metaKey='meta'] Key name for meta property.
* @param {boolean} [opts.stack=false] If true, then error stack trace will be included in the meta.
*
* @returns {Function}
*/module.exports=format((axiosInfo={},opts={...defaultOptions})=>{if(!axiosInfo||isObject(axiosInfo)===false||Object.keys(axiosInfo).length===0){// This is not a log object
return{}}const options=isObject(opts)&&Object.keys(opts).length?{...defaultOptions,...opts}:{...defaultOptions};const{enabled,metaKey}=options;if(!enabled){// Format is disabled, so we return the info object
return axiosInfo}if(!isAxiosError(axiosInfo)&&!isAxiosResponse(axiosInfo)&&!isAxiosRequest(axiosInfo)){// Not an axios Error/Request/Response
return axiosInfo}// Initialize result log object
const info={...topLevelInfo(axiosInfo),level:axiosInfo.level,[LEVEL]:axiosInfo[LEVEL]||axiosInfo.level,message:axiosInfo.message,[MESSAGE]:axiosInfo[MESSAGE]||axiosInfo.message,[metaKey]:{}};// Object with meta information for request or response
let meta;// req or res key in 'meta' object
let metaResultProp="res";if(isAxiosError(axiosInfo)){// Axios error
info.description=axiosInfo.description||options.errorDescription;if(axiosInfo.request&&!axiosInfo.response){// Request was made but no response was received
// `axiosInfo.request` is an instance of XMLHttpRequest in the browser and an instance of http.ClientRequest in node.js
// Used for errors like ECONNREFUSED, ECONNRESET, ENOTFOUND, etc.
// Technically there is no response here, but to make it more understandable in logs what was requested,
// and what was a "response" we make it compatible with response format.
// Example of what will be logged for the 'ENOTFOUND' error:
// {
// "description": "Axios Error",
// "label": "development",
// "level": "error",
// "message": "getaddrinfo ENOTFOUND media.xy",
// "meta": {
// "req": {
// "headers": {
// "Accept": "application/json, text/plain, */*",
// "User-Agent": "axios/1.3.3"
// },
// "method": "get",
// "requestStartedAt": 1673964871763,
// "timeout": "0",
// "url": "http://media.xy/eee",
// },
// "res": {
// "code": "ENOTFOUND",
// "status": null
// }
// },
// "timestamp": "2023-02-07 17:53:36.730"
// }
const{config,message,stack,code,status}=axiosInfo.toJSON();const{errno,syscall,hostname,address,port}=axiosInfo;info.message=message;if(options.meta){meta={errno,code,syscall,hostname,address,port,status,config,stack}}}if(axiosInfo.response){// Axios Error when server response with some status code
const{config:{url,method,baseURL},status,statusText,responseTime}=axiosInfo.response;info.message=[String(method).toUpperCase(),baseURL?new URL(url,baseURL).href:url,status,statusText,responseTime?`${responseTime}ms`:null].filter(Boolean).join(" ");// Example of what will be logged for the '404 Not Found' error:
// {
// "description": "Axios Error",
// "label": "development",
// "level": "error",
// "message": "GET http://localhost:1337/uploads/search_1bff605b0f1.svg 404 Not Found 18ms",
// "meta": {
// "req": {
// "headers": {
// "Accept": "application/json, text/plain, */*",
// "User-Agent": "axios/1.3.3"
// },
// "method": "get",
// "requestStartedAt": 1673964813765,
// "timeout": "0",
// "url": "http://localhost:1337/uploads/search_1bff605b0f1.svg",
// },
// "res": {
// "data": {
// "data": null,
// "error": {
// "details": {},
// "message": "Not Found",
// "name": "NotFoundError",
// "status": 404
// }
// },
// "headers": {
// "accept-ranges": "bytes",
// "content-length": "94",
// "content-type": "application/json; charset=utf-8",
// },
// "status": 404,
// "statusText": "Not Found"
// }
// },
// "timestamp": "2023-02-07 17:53:36.730"
// }
if(options.meta){meta={...axiosInfo.response}}}}if(isAxiosRequest(axiosInfo)){metaResultProp="req";info.description=axiosInfo.description||options.requestDescription;const request=axiosInfo.message;const{url,baseURL,method// Other available keys in req:
// data,
// headers,
}=request;info.message=[method.toUpperCase(),baseURL?new URL(url,baseURL).href:url].filter(Boolean).join(" ");if(options.meta){meta={...request}}}if(isAxiosResponse(axiosInfo)){// Axios request or response
info.description=axiosInfo.description||options.responseDescription;const{config:{url,method,baseURL},status,statusText,responseTime// Other available keys in res:
// data,
// headers,
}=axiosInfo.message;// Create a basic message with HTTP method, url, response code and response time:
// POST https://spin-tm-proxy.infotorg-eastnor-test.dds.evry.cloud/ 200 OK 189ms
info.message=[String(method).toUpperCase(),baseURL?new URL(url,baseURL).href:url,status,statusText,responseTime?`${responseTime}ms`:null// add response time if it exists
].filter(Boolean).join(" ");if(options.meta){meta={...axiosInfo.message}}}if(!options.meta){// No need to add meta information to the log message
return info}try{info[metaKey]=extractMeta(meta,metaResultProp,getWhiteListFields(options))}catch(e){// eslint-disable-next-line no-console
console.error(e)}return info});
//# sourceMappingURL=axios.js.map