diff --git a/CHANGELOG.md b/CHANGELOG.md index 6576c47..3dbaca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # CHANGELOG +## 2.2.1 (31.10.2023) +~ Resolve "_a.substring is not a function" error with applicationInsights. Issue was found in applicationinsights Library's TelemetryClient.trackTrace and EnvelopeFactory.createTraceData methods. This fix targets the compatibility issue observed in version 2.5.1 or higher. + ## 2.2.0 (31.10.2023) + Introduced responseValidation functionality in the middleware. This new feature enhances the robustness of your applications by enabling schema validation for handlers' responses. + Implemented the capability to dynamically enable or disable middleware functions within the execution flow. This addition brings conditional processing to your middleware stack, allowing greater control based on runtime conditions or application logic. Functions can now be seamlessly included or excluded from the execution process by resolving to true or false through a new integration pattern. This feature ensures that your application maintains high efficiency and adaptability in handling requests and processing logic. diff --git a/package-lock.json b/package-lock.json index 0ae6113..c49ea0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@senacor/azure-function-middleware", - "version": "2.2.0", + "version": "2.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@senacor/azure-function-middleware", - "version": "2.2.0", + "version": "2.2.1", "license": "MIT", "dependencies": { "@azure/functions": "^3.0.0", diff --git a/package.json b/package.json index 77d8ef0..ad68c86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@senacor/azure-function-middleware", - "version": "2.2.0", + "version": "2.2.1", "description": "Middleware for azure functions to handle authentication, authorization, error handling and logging", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/appInsights/Logger.ts b/src/appInsights/Logger.ts index dbea180..d96536d 100644 --- a/src/appInsights/Logger.ts +++ b/src/appInsights/Logger.ts @@ -2,60 +2,62 @@ import { Logger } from '@azure/functions'; import { TelemetryClient } from 'applicationinsights'; import { SeverityLevel } from 'applicationinsights/out/Declarations/Contracts'; -const consoleDefaultLog = (message: string): void => { - console.log(message); +import { stringify } from '../util/stringify'; + +const consoleDefaultLog = (...args: unknown[]): void => { + console.log(args); }; -consoleDefaultLog.error = (message: string): void => { - console.error(message); +consoleDefaultLog.error = (...args: unknown[]): void => { + console.error(args); }; -consoleDefaultLog.warn = (message: string): void => { - console.warn(message); +consoleDefaultLog.warn = (...args: unknown[]): void => { + console.warn(args); }; -consoleDefaultLog.info = (message: string): void => { - console.info(message); +consoleDefaultLog.info = (...args: unknown[]): void => { + console.info(args); }; -consoleDefaultLog.verbose = (message: string): void => { - console.debug(message); +consoleDefaultLog.verbose = (...args: unknown[]): void => { + console.debug(args); }; export const consoleLogger: Logger = consoleDefaultLog; export const createAppInsightsLogger = (telemetryClient: TelemetryClient): Logger => { - const appInsightsLogger = (message: string): void => { + const appInsightsLogger = (...args: unknown[]): void => { telemetryClient.trackTrace({ - message, + message: stringify(args), severity: SeverityLevel.Information, }); }; - appInsightsLogger.error = (message: string): void => { + appInsightsLogger.error = (...args: unknown[]): void => { telemetryClient.trackTrace({ - message, + message: stringify(args), severity: SeverityLevel.Error, }); }; - appInsightsLogger.warn = (message: string): void => { + appInsightsLogger.warn = (...args: unknown[]): void => { telemetryClient.trackTrace({ - message, + message: stringify(args), severity: SeverityLevel.Warning, }); }; - appInsightsLogger.info = (message: string): void => { + appInsightsLogger.info = (...args: unknown[]): void => { telemetryClient.trackTrace({ - message, + message: stringify(args), severity: SeverityLevel.Information, }); }; - appInsightsLogger.verbose = (message: string): void => { + appInsightsLogger.verbose = (...args: unknown[]): void => { telemetryClient.trackTrace({ - message, + message: stringify(args), severity: SeverityLevel.Verbose, }); }; diff --git a/src/error/errorHandler.ts b/src/error/errorHandler.ts index b6dbc92..091d837 100644 --- a/src/error/errorHandler.ts +++ b/src/error/errorHandler.ts @@ -1,45 +1,9 @@ import { Context } from '@azure/functions'; import { Options } from '../middleware'; +import { stringify } from '../util/stringify'; import { ApplicationError } from './ApplicationError'; -type ErrorWithMessage = { - message: string; - stack?: string; -}; - -const isErrorWithMessage = (error: unknown): error is ErrorWithMessage => { - return ( - typeof error === 'object' && - error !== null && - 'message' in error && - typeof (error as Record).message === 'string' - ); -}; - -const logErrorObject = (error: object | null, context: Context) => { - if (error === null) { - context.log.error('The provided error was eq to null - unable to log a specific error-message'); - return; - } - - if (isErrorWithMessage(error)) { - context.log.error({ message: error.message, stack: error.stack }); - } else { - try { - const errorAsJson = JSON.stringify(error); - if (errorAsJson === '{}') { - context.log.error(error.toString()); - } else { - context.log.error(errorAsJson); // Log the JSON string of the error object - } - } catch (_) { - //Fallback in case there's an error stringify - context.log.error(error.toString()); - } - } -}; - export const errorHandler = ( error: unknown, context: Context, @@ -55,16 +19,8 @@ export const errorHandler = ( }; } - switch (typeof error) { - case 'string': - context.log.error(error); - break; - case 'object': - logErrorObject(error, context); - break; - default: - context.log(`The error object has a type, that is not suitable for logging: ${typeof error}`); - } + const errorAsString = stringify(error); + context.log.error(errorAsString); if (opts?.errorResponseHandler === undefined) { return { diff --git a/src/util/stringify.test.ts b/src/util/stringify.test.ts new file mode 100644 index 0000000..f79b89c --- /dev/null +++ b/src/util/stringify.test.ts @@ -0,0 +1,63 @@ +import { ApplicationError } from '../error'; +import { stringify } from './stringify'; + +describe('stringify', () => { + describe('when provided with a single argument', () => { + it('returns the input string as is', () => { + const input = 'test string'; + expect(stringify(input)).toBe(input); + }); + + it('stringifies an Error object with message and stack', () => { + const error = new ApplicationError('conflict', 409); + const result = stringify(error); + expect(result).toContain(error.message); + }); + + it('stringifies a plain object correctly', () => { + const input = { key: 'value' }; + expect(stringify(input)).toBe(JSON.stringify(input)); + }); + + it('returns object.toString() for an empty object', () => { + const input = {}; + expect(stringify(input)).toBe('[object Object]'); + }); + + it('returns error message for null', () => { + expect(stringify(null)).toBe('The provided error was eq to null - unable to log a specific error-message'); + }); + + it('returns the number as a string for numbers', () => { + expect(stringify(123)).toBe('123'); + }); + + it('returns the boolean value as a string for booleans', () => { + expect(stringify(true)).toBe('true'); + expect(stringify(false)).toBe('false'); + }); + + it('handles symbols correctly', () => { + expect(stringify(Symbol())).toContain('not suitable for logging: symbol'); + }); + + it('handles functions correctly', () => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + expect(stringify(function () {})).toContain('not suitable for logging: function'); + }); + + it('handles cyclic objects by using toString', () => { + const obj: any = {}; + obj.cycle = obj; + expect(stringify(obj)).toBe(obj.toString()); + }); + }); + + describe('when provided with multiple arguments', () => { + it('joins stringified arguments with spaces', () => { + const arg1 = 'Hello'; + const arg2 = { message: 'World' }; + expect(stringify(arg1, arg2)).toBe('Hello {"message":"World"}'); + }); + }); +}); diff --git a/src/util/stringify.ts b/src/util/stringify.ts new file mode 100644 index 0000000..bb094ca --- /dev/null +++ b/src/util/stringify.ts @@ -0,0 +1,76 @@ +type ErrorWithMessage = { + message: string; + stack?: string; +}; + +const isErrorWithMessage = (error: unknown): error is ErrorWithMessage => { + return ( + typeof error === 'object' && + error !== null && + 'message' in error && + typeof (error as Record).message === 'string' + ); +}; + +const stringifyObject = (error: object | null): string => { + if (error === null) { + return 'The provided error was eq to null - unable to log a specific error-message'; + } + + if (error === undefined) { + return 'The provided error was eq to undefined - unable to log a specific error-message'; + } + + if (isErrorWithMessage(error)) { + return JSON.stringify({ message: error.message, stack: error.stack }); + } else { + try { + const errorAsJson = JSON.stringify(error); + if (errorAsJson === '{}') { + return error.toString(); + } else { + return errorAsJson; + } + } catch (_) { + //Fallback in case there's an error stringify + return error.toString(); + } + } +}; + +const unknownToString = (input: unknown) => { + if (typeof input === 'object' && input !== null && 'toString' in input) return input.toString(); + return String(input); +}; + +const stringifySingleArgument = (arg: unknown): string => { + switch (typeof arg) { + case 'string': + return arg; + case 'object': + return stringifyObject(arg); + case 'number': + return arg.toString(); + case 'boolean': + return String(arg); + default: + return `The error object with value ${unknownToString( + arg, + )} has a type, that is not suitable for logging: ${typeof arg}`; + } +}; + +export const stringify = (...args: unknown[]): string => { + switch (typeof args) { + case 'string': + return args; + case 'object': + if (Array.isArray(args)) { + return args.map((element) => stringifySingleArgument(element)).join(' '); + } + + return stringifyObject(args[0]); + default: + return `The error object has a type, that is not suitable for logging: ${typeof args}`; + } +};