From a2e27a26969c476c267490dd8d4bcd2cd80b56ad Mon Sep 17 00:00:00 2001 From: Eric Jizba Date: Thu, 2 May 2024 16:05:21 -0700 Subject: [PATCH] Support log hooks (#253) --- src/hooks/LogHookContext.ts | 51 ++++++++++++++++++++++++++++++ src/hooks/registerHook.ts | 25 ++++++++++++++- types-core/index.d.ts | 27 +++++++++++++++- types/InvocationContext.d.ts | 4 +-- types/hooks/logHooks.d.ts | 58 +++++++++++++++++++++++++++++++++++ types/hooks/registerHook.d.ts | 10 ++++++ types/index.d.ts | 3 ++ 7 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 src/hooks/LogHookContext.ts create mode 100644 types/hooks/logHooks.d.ts diff --git a/src/hooks/LogHookContext.ts b/src/hooks/LogHookContext.ts new file mode 100644 index 0000000..1f5e2ef --- /dev/null +++ b/src/hooks/LogHookContext.ts @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import * as types from '@azure/functions'; +import { ReadOnlyError } from '../errors'; +import { nonNullProp } from '../utils/nonNull'; +import { HookContext } from './HookContext'; + +export class LogHookContext extends HookContext implements types.LogHookContext { + #init: types.LogHookContextInit; + + constructor(init?: types.LogHookContextInit) { + super(init); + this.#init = init ?? {}; + this.#init.level ??= 'information'; + this.#init.message ??= 'unknown'; + this.#init.category ??= 'user'; + } + + get level(): types.LogLevel { + return nonNullProp(this.#init, 'level'); + } + + set level(value: types.LogLevel) { + this.#init.level = value; + } + + get message(): string { + return nonNullProp(this.#init, 'message'); + } + + set message(value: string) { + this.#init.message = value; + } + + get category(): types.LogCategory { + return nonNullProp(this.#init, 'category'); + } + + set category(_value: types.LogCategory) { + throw new ReadOnlyError('category'); + } + + get invocationContext(): types.InvocationContext | undefined { + return this.#init.invocationContext; + } + + set invocationContext(_value: types.InvocationContext | undefined) { + throw new ReadOnlyError('invocationContext'); + } +} diff --git a/src/hooks/registerHook.ts b/src/hooks/registerHook.ts index 446b928..944ef61 100644 --- a/src/hooks/registerHook.ts +++ b/src/hooks/registerHook.ts @@ -1,12 +1,20 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. -import { AppStartHandler, AppTerminateHandler, PostInvocationHandler, PreInvocationHandler } from '@azure/functions'; +import { + AppStartHandler, + AppTerminateHandler, + LogHookHandler, + PostInvocationHandler, + PreInvocationHandler, +} from '@azure/functions'; import * as coreTypes from '@azure/functions-core'; +import { AzFuncSystemError, ensureErrorType } from '../errors'; import { Disposable } from '../utils/Disposable'; import { tryGetCoreApiLazy } from '../utils/tryGetCoreApiLazy'; import { AppStartContext } from './AppStartContext'; import { AppTerminateContext } from './AppTerminateContext'; +import { LogHookContext } from './LogHookContext'; import { PostInvocationContext } from './PostInvocationContext'; import { PreInvocationContext } from './PreInvocationContext'; @@ -49,3 +57,18 @@ export function postInvocation(handler: PostInvocationHandler): Disposable { return handler(new PostInvocationContext(coreContext)); }); } + +export function log(handler: LogHookHandler): Disposable { + try { + return registerHook('log', (coreContext) => { + return handler(new LogHookContext(coreContext)); + }); + } catch (err) { + const error = ensureErrorType(err); + if (error.name === 'RangeError' && error.isAzureFunctionsSystemError) { + throw new AzFuncSystemError(`Log hooks require Azure Functions Host v4.34 or higher.`); + } else { + throw err; + } + } +} diff --git a/types-core/index.d.ts b/types-core/index.d.ts index e5e394f..2fb3506 100644 --- a/types-core/index.d.ts +++ b/types-core/index.d.ts @@ -58,13 +58,15 @@ declare module '@azure/functions-core' { function registerHook(hookName: 'postInvocation', callback: PostInvocationCallback): Disposable; function registerHook(hookName: 'appStart', callback: AppStartCallback): Disposable; function registerHook(hookName: 'appTerminate', callback: AppTerminateCallback): Disposable; + function registerHook(hookName: 'log', callback: LogHookCallback): Disposable; function registerHook(hookName: string, callback: HookCallback): Disposable; - type HookCallback = (context: HookContext) => void | Promise; + type HookCallback = (context: HookContext) => unknown; type PreInvocationCallback = (context: PreInvocationContext) => void | Promise; type PostInvocationCallback = (context: PostInvocationContext) => void | Promise; type AppStartCallback = (context: AppStartContext) => void | Promise; type AppTerminateCallback = (context: AppTerminateContext) => void | Promise; + type LogHookCallback = (context: LogHookContext) => void; type HookData = { [key: string]: any }; @@ -146,6 +148,29 @@ declare module '@azure/functions-core' { type AppTerminateContext = HookContext; + interface LogHookContext extends HookContext { + /** + * If the log occurs during a function execution, the context object passed to the function handler. + * Otherwise, undefined. + */ + readonly invocationContext?: unknown; + + /** + * 'system' if the log is generated by Azure Functions, 'user' if the log is generated by your own app. + */ + readonly category: RpcLogCategory; + + /** + * Changes to this value _will_ affect the resulting log, but only for user-generated logs. + */ + level: RpcLogLevel; + + /** + * Changes to this value _will_ affect the resulting log, but only for user-generated logs. + */ + message: string; + } + /** * Represents a type which can release resources, such as event listening or a timer. */ diff --git a/types/InvocationContext.d.ts b/types/InvocationContext.d.ts index 533e2f4..cbd6e40 100644 --- a/types/InvocationContext.d.ts +++ b/types/InvocationContext.d.ts @@ -5,7 +5,7 @@ import { CosmosDBInput, CosmosDBOutput } from './cosmosDB'; import { EventGridOutput, EventGridPartialEvent } from './eventGrid'; import { EventHubOutput } from './eventHub'; import { HttpOutput, HttpResponse } from './http'; -import { FunctionInput, FunctionOutput, FunctionTrigger } from './index'; +import { FunctionInput, FunctionOutput, FunctionTrigger, LogLevel } from './index'; import { ServiceBusQueueOutput, ServiceBusTopicOutput } from './serviceBus'; import { SqlInput, SqlOutput } from './sql'; import { StorageBlobInput, StorageBlobOutput, StorageQueueOutput } from './storage'; @@ -342,5 +342,3 @@ export interface InvocationContextInit { } export type LogHandler = (level: LogLevel, ...args: unknown[]) => void; - -export type LogLevel = 'trace' | 'debug' | 'information' | 'warning' | 'error' | 'critical' | 'none'; diff --git a/types/hooks/logHooks.d.ts b/types/hooks/logHooks.d.ts new file mode 100644 index 0000000..a223c59 --- /dev/null +++ b/types/hooks/logHooks.d.ts @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import { LogLevel } from '../index'; +import { InvocationContext } from '../InvocationContext'; +import { HookContext, HookContextInit } from './HookContext'; + +/** + * Handler for log hooks. + */ +export type LogHookHandler = (context: LogHookContext) => void; + +/** + * Context on a log + */ +export declare class LogHookContext extends HookContext { + /** + * For testing purposes only. This will always be constructed for you when run in the context of the Azure Functions runtime + */ + constructor(init?: LogHookContextInit); + + /** + * If the log occurs during a function execution, the context object passed to the function handler. + * Otherwise, undefined. + */ + readonly invocationContext: InvocationContext | undefined; + + /** + * 'system' if the log is generated by Azure Functions, 'user' if the log is generated by your own app. + */ + readonly category: LogCategory; + + /** + * Changes to this value _will_ affect the resulting log, but only for user-generated logs. + */ + level: LogLevel; + + /** + * Changes to this value _will_ affect the resulting log, but only for user-generated logs. + */ + message: string; +} + +/** + * Object passed to LogHookContext constructors. + * For testing purposes only + */ +export interface LogHookContextInit extends HookContextInit { + invocationContext?: InvocationContext; + + level?: LogLevel; + + category?: LogCategory; + + message?: string; +} + +export type LogCategory = 'user' | 'system' | 'customMetric'; diff --git a/types/hooks/registerHook.d.ts b/types/hooks/registerHook.d.ts index 8c60e51..c736687 100644 --- a/types/hooks/registerHook.d.ts +++ b/types/hooks/registerHook.d.ts @@ -4,6 +4,7 @@ import { Disposable } from '../index'; import { AppStartHandler, AppTerminateHandler } from './appHooks'; import { PostInvocationHandler, PreInvocationHandler } from './invocationHooks'; +import { LogHookHandler } from './logHooks'; /** * Register a hook to be run at the start of your application @@ -38,3 +39,12 @@ export function preInvocation(handler: PreInvocationHandler): Disposable; * @returns a `Disposable` object that can be used to unregister the hook */ export function postInvocation(handler: PostInvocationHandler): Disposable; + +/** + * PREVIEW: Register a hook to be run for each log. + * This functionality requires Azure Functions Host v4.34+. + * + * @param handler the handler for the hook + * @returns a `Disposable` object that can be used to unregister the hook + */ +export function log(handler: LogHookHandler): Disposable; diff --git a/types/index.d.ts b/types/index.d.ts index 4a3d990..84a03eb 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -13,6 +13,7 @@ export * from './generic'; export * from './hooks/appHooks'; export * from './hooks/HookContext'; export * from './hooks/invocationHooks'; +export * from './hooks/logHooks'; export * from './http'; export * as input from './input'; export * from './InvocationContext'; @@ -198,3 +199,5 @@ export declare class Disposable { */ dispose(): any; } + +export type LogLevel = 'trace' | 'debug' | 'information' | 'warning' | 'error' | 'critical' | 'none';