Skip to content

Commit bccdce3

Browse files
authored
Add hook for logs (#739)
1 parent 456120c commit bccdce3

File tree

9 files changed

+154
-12
lines changed

9 files changed

+154
-12
lines changed

src/AppContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class AppContext {
2929
postInvocationHooks: HookCallback[] = [];
3030
appStartHooks: HookCallback[] = [];
3131
appTerminateHooks: HookCallback[] = [];
32+
logHooks: HookCallback[] = [];
3233
functions: { [id: string]: RegisteredFunction } = {};
3334
legacyFunctions: { [id: string]: LegacyRegisteredFunction } = {};
3435
workerIndexingLocked = false;

src/WorkerContext.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import { ProgrammingModel } from '@azure/functions-core';
55
import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc';
66
import { AppContext } from './AppContext';
7+
import { fromCoreLogLevel } from './coreApi/converters/fromCoreStatusResult';
78
import { AzFuncSystemError } from './errors';
89
import { IEventStream } from './GrpcClient';
10+
import { InvocationLogContext, LogHookContext } from './hooks/LogHookContext';
911

1012
class WorkerContext {
1113
app = new AppContext();
@@ -62,7 +64,23 @@ class WorkerContext {
6264
* @param requestId gRPC message request id
6365
* @param msg gRPC message content
6466
*/
65-
log(log: rpc.IRpcLog) {
67+
log(log: rpc.IRpcLog, invocationLogCtx?: InvocationLogContext): void {
68+
try {
69+
const logContext = new LogHookContext(log, invocationLogCtx);
70+
for (const callback of worker.app.logHooks) {
71+
callback(logContext);
72+
}
73+
74+
if (log.logCategory === rpc.RpcLog.RpcLogCategory.User) {
75+
// let hooks change and filter these values, but only for user-generated logs
76+
// system logs should always be sent as-is
77+
log.message = logContext.message;
78+
log.level = fromCoreLogLevel(logContext.level);
79+
}
80+
} catch {
81+
// ignore so that user hooks can't prevent system logs
82+
}
83+
6684
this.eventStream.write({
6785
rpcLog: log,
6886
});

src/coreApi/converters/toCoreStatusResult.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function toCoreLogs(data: rpc.IRpcLog[] | null | undefined): coreTypes.RpcLog[]
2929
}
3030
}
3131

32-
function toCoreLog(data: rpc.IRpcLog): coreTypes.RpcLog {
32+
export function toCoreLog(data: rpc.IRpcLog): coreTypes.RpcLog {
3333
const result = {
3434
...data,
3535
level: toCoreLogLevel(data.level),

src/eventHandlers/InvocationHandler.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { toCoreFunctionMetadata } from '../coreApi/converters/toCoreFunctionMeta
2121
import { toCoreInvocationRequest } from '../coreApi/converters/toCoreInvocationRequest';
2222
import { AzFuncSystemError, isError, ReadOnlyError } from '../errors';
2323
import { executeHooks } from '../hooks/executeHooks';
24+
import { InvocationLogContext } from '../hooks/LogHookContext';
2425
import { getLegacyFunction } from '../LegacyFunctionLoader';
2526
import { nonNullProp } from '../utils/nonNull';
2627
import { worker } from '../WorkerContext';
@@ -70,6 +71,7 @@ export class InvocationHandler extends EventHandler<'invocationRequest', 'invoca
7071

7172
const hookData: HookData = {};
7273
let { context, inputs } = await invocModel.getArguments();
74+
coreCtx.logContext = { hookData, invocationContext: context };
7375

7476
const preInvocContext: PreInvocationContext = {
7577
get hookData() {
@@ -157,6 +159,7 @@ class CoreInvocationContext implements coreTypes.CoreInvocationContext {
157159
request: RpcInvocationRequest;
158160
metadata: RpcFunctionMetadata;
159161
state?: InvocationState;
162+
logContext?: InvocationLogContext;
160163
#msgCategory: string;
161164

162165
constructor(request: RpcInvocationRequest, metadata: RpcFunctionMetadata, msgCategory: string) {
@@ -167,12 +170,15 @@ class CoreInvocationContext implements coreTypes.CoreInvocationContext {
167170
}
168171

169172
log(level: RpcLogLevel, logCategory: RpcLogCategory, message: string): void {
170-
worker.log({
171-
invocationId: this.request.invocationId,
172-
category: this.#msgCategory,
173-
message,
174-
level: fromCoreLogLevel(level),
175-
logCategory: fromCoreLogCategory(logCategory),
176-
});
173+
worker.log(
174+
{
175+
invocationId: this.request.invocationId,
176+
category: this.#msgCategory,
177+
message,
178+
level: fromCoreLogLevel(level),
179+
logCategory: fromCoreLogCategory(logCategory),
180+
},
181+
this.logContext
182+
);
177183
}
178184
}

src/hooks/LogHookContext.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { HookData, RpcLogCategory, RpcLogLevel } from '@azure/functions-core';
5+
import * as coreTypes from '@azure/functions-core';
6+
import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc';
7+
import { toCoreLog } from '../coreApi/converters/toCoreStatusResult';
8+
import { ReadOnlyError } from '../errors';
9+
import { nonNullProp } from '../utils/nonNull';
10+
import { worker } from '../WorkerContext';
11+
12+
export interface InvocationLogContext {
13+
hookData: HookData;
14+
invocationContext: unknown;
15+
}
16+
17+
export class LogHookContext implements coreTypes.LogHookContext {
18+
level: RpcLogLevel;
19+
message: string;
20+
#category: RpcLogCategory;
21+
#hookData: HookData;
22+
#invocationContext: unknown;
23+
24+
constructor(log: rpc.IRpcLog, invocLogCtx: InvocationLogContext | undefined) {
25+
const coreLog = toCoreLog(log);
26+
this.level = nonNullProp(coreLog, 'level');
27+
this.message = nonNullProp(coreLog, 'message');
28+
this.#category = nonNullProp(coreLog, 'logCategory');
29+
this.#hookData = invocLogCtx?.hookData ?? {};
30+
this.#invocationContext = invocLogCtx?.invocationContext;
31+
}
32+
33+
get hookData(): HookData {
34+
return this.#hookData;
35+
}
36+
set hookData(_obj: HookData) {
37+
throw new ReadOnlyError('hookData');
38+
}
39+
get category(): RpcLogCategory {
40+
return this.#category;
41+
}
42+
set category(_obj: RpcLogCategory) {
43+
throw new ReadOnlyError('category');
44+
}
45+
get appHookData(): HookData {
46+
return worker.app.appHookData;
47+
}
48+
set appHookData(_obj: HookData) {
49+
throw new ReadOnlyError('appHookData');
50+
}
51+
get invocationContext(): unknown {
52+
return this.#invocationContext;
53+
}
54+
set invocationContext(_obj: unknown) {
55+
throw new ReadOnlyError('invocationContext');
56+
}
57+
}

src/hooks/getHooks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export function getHooks(hookName: string): HookCallback[] {
1515
return worker.app.appStartHooks;
1616
case 'appTerminate':
1717
return worker.app.appTerminateHooks;
18+
case 'log':
19+
return worker.app.logHooks;
1820
default:
1921
throw new AzFuncRangeError(`Unrecognized hook "${hookName}"`);
2022
}

test/eventHandlers/InvocationHandler.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,4 +1063,37 @@ describe('InvocationHandler', () => {
10631063
msg.invocation.response([])
10641064
);
10651065
});
1066+
1067+
it('log hook respects changes to value, only for user log', async () => {
1068+
coreApi.registerHook('log', (ctx) => {
1069+
ctx.message += 'UpdatedFromHook';
1070+
ctx.level = 'error';
1071+
});
1072+
1073+
registerV3Func(Binding.queue, async (invocContext: Context) => {
1074+
invocContext.log('testUserLog');
1075+
});
1076+
stream.addTestMessage(msg.invocation.request([InputData.http]));
1077+
await stream.assertCalledWith(
1078+
msg.invocation.receivedRequestLog,
1079+
msg.invocation.userLog('testUserLogUpdatedFromHook', LogLevel.Error),
1080+
msg.invocation.response([])
1081+
);
1082+
});
1083+
1084+
it('ignores log hook error', async () => {
1085+
coreApi.registerHook('log', (_ctx) => {
1086+
throw new Error('failed log hook');
1087+
});
1088+
1089+
registerV3Func(Binding.queue, async (invocContext: Context) => {
1090+
invocContext.log('testUserLog');
1091+
});
1092+
stream.addTestMessage(msg.invocation.request([InputData.http]));
1093+
await stream.assertCalledWith(
1094+
msg.invocation.receivedRequestLog,
1095+
msg.invocation.userLog(),
1096+
msg.invocation.response([])
1097+
);
1098+
});
10661099
});

test/eventHandlers/msg.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,13 +381,13 @@ export namespace msg {
381381
"Warning: Unexpected call to 'log' on the context object after function execution has completed. Please check for asynchronous calls that are not awaited or calls to 'done' made before function execution completes. Function name: testFuncName. Invocation Id: 1. Learn more: https://go.microsoft.com/fwlink/?linkid=2097909"
382382
);
383383

384-
export function userLog(data = 'testUserLog'): TestMessage {
384+
export function userLog(data = 'testUserLog', level = LogLevel.Information): TestMessage {
385385
return {
386386
rpcLog: {
387387
category: 'testFuncName.Invocation',
388388
invocationId: '1',
389389
message: data,
390-
level: LogLevel.Information,
390+
level,
391391
logCategory: LogCategory.User,
392392
},
393393
};

types-core/index.d.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,15 @@ declare module '@azure/functions-core' {
5858
function registerHook(hookName: 'postInvocation', callback: PostInvocationCallback): Disposable;
5959
function registerHook(hookName: 'appStart', callback: AppStartCallback): Disposable;
6060
function registerHook(hookName: 'appTerminate', callback: AppTerminateCallback): Disposable;
61+
function registerHook(hookName: 'log', callback: LogHookCallback): Disposable;
6162
function registerHook(hookName: string, callback: HookCallback): Disposable;
6263

63-
type HookCallback = (context: HookContext) => void | Promise<void>;
64+
type HookCallback = (context: HookContext) => unknown;
6465
type PreInvocationCallback = (context: PreInvocationContext) => void | Promise<void>;
6566
type PostInvocationCallback = (context: PostInvocationContext) => void | Promise<void>;
6667
type AppStartCallback = (context: AppStartContext) => void | Promise<void>;
6768
type AppTerminateCallback = (context: AppTerminateContext) => void | Promise<void>;
69+
type LogHookCallback = (context: LogHookContext) => void;
6870

6971
type HookData = { [key: string]: any };
7072

@@ -146,6 +148,29 @@ declare module '@azure/functions-core' {
146148

147149
type AppTerminateContext = HookContext;
148150

151+
interface LogHookContext extends HookContext {
152+
/**
153+
* If the log occurs during a function execution, the context object passed to the function handler.
154+
* Otherwise, undefined.
155+
*/
156+
readonly invocationContext?: unknown;
157+
158+
/**
159+
* 'system' if the log is generated by Azure Functions, 'user' if the log is generated by your own app.
160+
*/
161+
readonly category: RpcLogCategory;
162+
163+
/**
164+
* Changes to this value _will_ affect the resulting log, but only for user-generated logs.
165+
*/
166+
level: RpcLogLevel;
167+
168+
/**
169+
* Changes to this value _will_ affect the resulting log, but only for user-generated logs.
170+
*/
171+
message: string;
172+
}
173+
149174
/**
150175
* Represents a type which can release resources, such as event listening or a timer.
151176
*/

0 commit comments

Comments
 (0)