Skip to content

Commit 11b2861

Browse files
authored
Modify EoL log for Consumption (#775)
Add EoL Warn/Error log on startup
1 parent 5205801 commit 11b2861

12 files changed

+152
-57
lines changed

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the MIT License.
33

44
export const version = '3.11.1';
5+
export const logPrefix = 'LanguageWorkerConsoleLog';
6+
export const upgradeUrl = 'https://aka.ms/functions-nodejs-supported-versions';
57

68
// https://github.com/nodejs/Release
79
export const NODE_EOL_DATES: Record<string, string> = {

src/eventHandlers/FunctionEnvironmentReloadHandler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getWorkerCapabilities } from './getWorkerCapabilities';
99
import { getWorkerMetadata } from './getWorkerMetadata';
1010
import CapabilitiesUpdateStrategy = rpc.FunctionEnvironmentReloadResponse.CapabilitiesUpdateStrategy;
1111
import * as path from 'path';
12+
import { validateNodeVersion } from '../utils/util';
1213

1314
/**
1415
* Environment variables from the current process
@@ -76,6 +77,7 @@ export class FunctionEnvironmentReloadHandler extends EventHandler<
7677
response.workerMetadata = getWorkerMetadata();
7778
}
7879

80+
validateNodeVersion(process.version);
7981
response.capabilities = await getWorkerCapabilities();
8082
response.capabilitiesUpdateStrategy = CapabilitiesUpdateStrategy.replace;
8183

src/eventHandlers/WorkerInitHandler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language
77
import { isError } from '../errors';
88
import { startApp } from '../startApp';
99
import { nonNullProp } from '../utils/nonNull';
10+
import { validateNodeVersion } from '../utils/util';
1011
import { worker } from '../WorkerContext';
1112
import { EventHandler } from './EventHandler';
1213
import { getWorkerCapabilities } from './getWorkerCapabilities';
@@ -52,6 +53,7 @@ export class WorkerInitHandler extends EventHandler<'workerInitRequest', 'worker
5253
response.workerMetadata = getWorkerMetadata();
5354
}
5455

56+
validateNodeVersion(process.version);
5557
response.capabilities = await getWorkerCapabilities();
5658

5759
return response;

src/nodejsWorker.ts

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,10 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import { NODE_EOL_DATES, NODE_EOL_WARNING_DATES } from './constants';
4+
import { logPrefix } from './constants';
55

6-
const logPrefix = 'LanguageWorkerConsoleLog';
7-
const errorPrefix = logPrefix + '[error] ';
8-
const warnPrefix = logPrefix + '[warn] ';
9-
const upgradeUrl = 'https://aka.ms/functions-nodejs-supported-versions';
106
let workerModule;
117

12-
function currentYearMonth(): string {
13-
const now = new Date();
14-
return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}`;
15-
}
16-
17-
// Try validating node version
18-
// NOTE: This method should be manually tested if changed as it is in a sensitive code path
19-
// and is JavaScript that runs on at least node version 0.10.28
20-
function validateNodeVersion(version: string) {
21-
try {
22-
const versionSplit = version.split('.');
23-
if (versionSplit.length != 3) {
24-
throw new Error("Could not parse Node.js version: '" + version + "'");
25-
}
26-
27-
const major = versionSplit[0]; // e.g. "v18"
28-
const warningDateStr = NODE_EOL_WARNING_DATES[major];
29-
const eolDateStr = NODE_EOL_DATES[major];
30-
const today = currentYearMonth();
31-
if (!warningDateStr || !eolDateStr) {
32-
const msg = `Incompatible Node.js version ${major}. Refer to our documentation to see the Node.js versions supported by each version of Azure Functions: ${upgradeUrl}`;
33-
console.warn(warnPrefix + msg);
34-
} else if (today >= eolDateStr) {
35-
const msg = `Node.js ${major} reached EOL on ${eolDateStr}. Please upgrade to a supported version: ${upgradeUrl}`;
36-
console.error(errorPrefix + msg);
37-
} else if (today >= warningDateStr) {
38-
const msg = `Node.js ${major} will reach EOL on ${eolDateStr}. Consider upgrading: ${upgradeUrl}`;
39-
console.warn(warnPrefix + msg);
40-
}
41-
} catch (err) {
42-
const unknownError = 'Error validating Node.js version. ';
43-
console.error(errorPrefix + unknownError + err);
44-
throw err;
45-
}
46-
}
47-
48-
validateNodeVersion(process.version);
49-
508
// Try requiring bundle
519
try {
5210
workerModule = require('./worker-bundle.js');

src/utils/util.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// Licensed under the MIT License.
33

44
import * as semver from 'semver';
5+
import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc';
6+
import { NODE_EOL_DATES, NODE_EOL_WARNING_DATES, upgradeUrl } from '../constants';
7+
import { worker } from '../WorkerContext';
58

69
export function isEnvironmentVariableSet(val: string | boolean | number | undefined | null): boolean {
710
return !/^(false|0)?$/i.test(val === undefined || val === null ? '' : String(val));
@@ -10,3 +13,65 @@ export function isEnvironmentVariableSet(val: string | boolean | number | undefi
1013
export function isNode20Plus(): boolean {
1114
return semver.gte(process.versions.node, '20.0.0');
1215
}
16+
17+
function currentYearMonth(): string {
18+
const now = new Date();
19+
return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}`;
20+
}
21+
22+
interface NodeVersionLog {
23+
message: string;
24+
level: rpc.RpcLog.Level;
25+
}
26+
27+
export function getNodeVersionLog(version: string): NodeVersionLog | undefined {
28+
const versionSplit = version.split('.');
29+
if (versionSplit.length != 3) {
30+
throw new Error("Could not parse Node.js version: '" + version + "'");
31+
}
32+
33+
const major = versionSplit[0]; // e.g. "v18"
34+
const warningDateStr = NODE_EOL_WARNING_DATES[major];
35+
const eolDateStr = NODE_EOL_DATES[major];
36+
const today = currentYearMonth();
37+
if (!warningDateStr || !eolDateStr) {
38+
const msg = `Incompatible Node.js version ${major}. Refer to our documentation to see the Node.js versions supported by each version of Azure Functions: ${upgradeUrl}`;
39+
return {
40+
message: msg,
41+
level: rpc.RpcLog.Level.Warning,
42+
};
43+
} else if (today >= eolDateStr) {
44+
const msg = `Node.js ${major} reached EOL on ${eolDateStr}. Please upgrade to a supported version: ${upgradeUrl}`;
45+
return {
46+
message: msg,
47+
level: rpc.RpcLog.Level.Error,
48+
};
49+
} else if (today >= warningDateStr) {
50+
const msg = `Node.js ${major} will reach EOL on ${eolDateStr}. Consider upgrading: ${upgradeUrl}`;
51+
return {
52+
message: msg,
53+
level: rpc.RpcLog.Level.Warning,
54+
};
55+
}
56+
return undefined;
57+
}
58+
59+
export function validateNodeVersion(version: string) {
60+
try {
61+
const logEntry = getNodeVersionLog(version);
62+
if (logEntry) {
63+
worker.log({
64+
message: logEntry.message,
65+
level: logEntry.level,
66+
logCategory: rpc.RpcLog.RpcLogCategory.System,
67+
});
68+
}
69+
} catch (err) {
70+
worker.log({
71+
message: 'Error validating Node.js version. ' + err,
72+
level: rpc.RpcLog.Level.Error,
73+
logCategory: rpc.RpcLog.RpcLogCategory.System,
74+
});
75+
throw err;
76+
}
77+
}

test/eventHandlers/FunctionEnvironmentReloadHandler.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ describe('FunctionEnvironmentReloadHandler', () => {
2323

2424
async function mockPlaceholderInit(): Promise<void> {
2525
stream.addTestMessage(msg.init.request('pathWithoutPackageJson'));
26-
await stream.assertCalledWith(msg.init.receivedRequestLog, msg.noPackageJsonWarning, msg.init.response);
26+
await stream.assertCalledWith(
27+
msg.init.receivedRequestLog,
28+
msg.noPackageJsonWarning,
29+
msg.init.nodeVersionLog(),
30+
msg.init.response
31+
);
2732
}
2833

2934
it('reloads environment variables', async () => {
@@ -41,6 +46,7 @@ describe('FunctionEnvironmentReloadHandler', () => {
4146
await stream.assertCalledWith(
4247
msg.envReload.funcAppDirNotDefined,
4348
msg.envReload.reloadEnvVarsLog(2),
49+
msg.envReload.nodeVersionLog(),
4450
msg.envReload.response
4551
);
4652
expect(process.env.hello).to.equal('world');
@@ -63,6 +69,7 @@ describe('FunctionEnvironmentReloadHandler', () => {
6369
await stream.assertCalledWith(
6470
msg.envReload.funcAppDirNotDefined,
6571
msg.envReload.reloadEnvVarsLog(2),
72+
msg.envReload.nodeVersionLog(),
6673
msg.envReload.response
6774
);
6875
expect(process.env.hello).to.equal('world');
@@ -91,6 +98,7 @@ describe('FunctionEnvironmentReloadHandler', () => {
9198
await stream.assertCalledWith(
9299
msg.envReload.funcAppDirNotDefined,
93100
msg.envReload.reloadEnvVarsLog(0),
101+
msg.envReload.nodeVersionLog(),
94102
msg.envReload.response
95103
);
96104
expect(process.env).to.be.empty;
@@ -107,6 +115,7 @@ describe('FunctionEnvironmentReloadHandler', () => {
107115
await stream.assertCalledWith(
108116
msg.envReload.funcAppDirNotDefined,
109117
msg.envReload.reloadEnvVarsLog(0),
118+
msg.envReload.nodeVersionLog(),
110119
msg.envReload.response
111120
);
112121

@@ -127,6 +136,7 @@ describe('FunctionEnvironmentReloadHandler', () => {
127136
await stream.assertCalledWith(
128137
msg.envReload.funcAppDirNotDefined,
129138
msg.envReload.reloadEnvVarsLog(0),
139+
msg.envReload.nodeVersionLog(),
130140
msg.envReload.response
131141
);
132142
});
@@ -146,6 +156,7 @@ describe('FunctionEnvironmentReloadHandler', () => {
146156
await stream.assertCalledWith(
147157
msg.envReload.funcAppDirNotDefined,
148158
msg.envReload.reloadEnvVarsLog(2),
159+
msg.envReload.nodeVersionLog(),
149160
msg.envReload.response
150161
);
151162
expect(process.env.hello).to.equal('world');
@@ -171,6 +182,7 @@ describe('FunctionEnvironmentReloadHandler', () => {
171182
msg.envReload.reloadEnvVarsLog(2),
172183
msg.envReload.changingCwdLog(),
173184
msg.noPackageJsonWarning,
185+
msg.envReload.nodeVersionLog(),
174186
msg.envReload.response
175187
);
176188
expect(process.env.hello).to.equal('world');
@@ -192,6 +204,7 @@ describe('FunctionEnvironmentReloadHandler', () => {
192204
await stream.assertCalledWith(
193205
msg.envReload.reloadEnvVarsLog(0),
194206
msg.envReload.changingCwdLog(testAppPath),
207+
msg.envReload.nodeVersionLog(),
195208
msg.envReload.response
196209
);
197210
expect(worker.app.packageJson).to.deep.equal(oldPackageJson);
@@ -208,6 +221,7 @@ describe('FunctionEnvironmentReloadHandler', () => {
208221
msg.envReload.funcAppDirNotChanged,
209222
msg.envReload.reloadEnvVarsLog(0),
210223
msg.envReload.changingCwdLog(testAppPath),
224+
msg.envReload.nodeVersionLog(),
211225
msg.envReload.response
212226
);
213227
expect(worker.app.packageJson).to.deep.equal(newPackageJson);
@@ -229,6 +243,7 @@ describe('FunctionEnvironmentReloadHandler', () => {
229243
await stream.assertCalledWith(
230244
msg.envReload.reloadEnvVarsLog(0),
231245
msg.envReload.changingCwdLog(testAppPath),
246+
msg.envReload.nodeVersionLog(),
232247
msg.envReload.response
233248
);
234249
expect(worker.app.packageJson).to.deep.equal(packageJson);
@@ -251,6 +266,7 @@ describe('FunctionEnvironmentReloadHandler', () => {
251266
msg.envReload.changingCwdLog(testAppPath),
252267
msg.loadingEntryPoint(fileSubpath),
253268
msg.loadedEntryPoint(fileSubpath),
269+
msg.envReload.nodeVersionLog(),
254270
msg.envReload.response
255271
);
256272
});
@@ -283,6 +299,7 @@ describe('FunctionEnvironmentReloadHandler', () => {
283299
msg.envReload.changingCwdLog(testAppPath),
284300
msg.loadingEntryPoint(fileSubpath),
285301
msg.loadedEntryPoint(fileSubpath),
302+
msg.envReload.nodeVersionLog(),
286303
msg.envReload.response
287304
);
288305

@@ -323,6 +340,7 @@ describe('FunctionEnvironmentReloadHandler', () => {
323340
msg.loadedEntryPoint(fileSubpath),
324341
msg.executingAppHooksLog(1, 'appStart'),
325342
msg.executedAppHooksLog('appStart'),
343+
msg.envReload.nodeVersionLog(),
326344
msg.envReload.response
327345
);
328346
});

test/eventHandlers/InvocationHandler.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,7 @@ describe('InvocationHandler', () => {
727727
msg.noPackageJsonWarning,
728728
msg.executingAppHooksLog(1, 'appStart'),
729729
msg.executedAppHooksLog('appStart'),
730+
msg.init.nodeVersionLog(),
730731
msg.init.response
731732
);
732733
expect(startFunc.callCount).to.be.equal(1);
@@ -821,6 +822,7 @@ describe('InvocationHandler', () => {
821822
msg.noPackageJsonWarning,
822823
msg.executingAppHooksLog(1, 'appStart'),
823824
msg.executedAppHooksLog('appStart'),
825+
msg.init.nodeVersionLog(),
824826
msg.init.response
825827
);
826828
expect(startFunc.callCount).to.be.equal(1);

test/eventHandlers/TestEventStream.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,40 +33,46 @@ export class TestEventStream extends EventEmitter implements IEventStream {
3333
/**
3434
* Waits up to a second for the expected number of messages to be written and then validates those messages
3535
*/
36-
async assertCalledWith(...expectedMsgs: (rpc.IStreamingMessage | RegExpStreamingMessage)[]): Promise<void> {
36+
async assertCalledWith(
37+
...expectedMsgs: (rpc.IStreamingMessage | RegExpStreamingMessage | undefined)[]
38+
): Promise<void> {
3739
try {
40+
const filteredExpectedMsgs: (rpc.IStreamingMessage | RegExpStreamingMessage)[] = expectedMsgs.filter(
41+
(m): m is rpc.IStreamingMessage | RegExpStreamingMessage => m !== undefined
42+
);
43+
3844
// Wait for up to a second for the expected number of messages to come in
3945
const maxTime = Date.now() + 1000;
4046
const interval = 10;
41-
while (this.written.getCalls().length < expectedMsgs.length && Date.now() < maxTime) {
47+
while (this.written.getCalls().length < filteredExpectedMsgs.length && Date.now() < maxTime) {
4248
await new Promise((resolve) => setTimeout(resolve, interval));
4349
}
4450

4551
const calls = this.written.getCalls();
4652

4753
// First, validate the "shortened" form of the messages. This will result in a more readable error for most test failures
4854
if (
49-
!expectedMsgs.find((m) => m instanceof RegExpStreamingMessage) ||
50-
calls.length !== expectedMsgs.length
55+
!filteredExpectedMsgs.find((m) => m instanceof RegExpStreamingMessage) ||
56+
calls.length !== filteredExpectedMsgs.length
5157
) {
5258
// shortened message won't work if it's a regexp
5359
// but if the call count doesn't match, this error will be better than the one below
54-
const shortExpectedMsgs = expectedMsgs.map(getShortenedMsg);
60+
const shortExpectedMsgs = filteredExpectedMsgs.map(getShortenedMsg);
5561
const shortActualMsgs = calls.map((c) => getShortenedMsg(c.args[0]));
5662
expect(shortActualMsgs).to.deep.equal(shortExpectedMsgs);
5763
}
5864

5965
// Next, do a more comprehensive check on the messages
6066
expect(calls.length).to.equal(
61-
expectedMsgs.length,
67+
filteredExpectedMsgs.length,
6268
'Message count does not match. This may be caused by the previous test writing extraneous messages.'
6369
);
64-
for (let i = 0; i < expectedMsgs.length; i++) {
70+
for (let i = 0; i < filteredExpectedMsgs.length; i++) {
6571
const call = calls[i];
6672
expect(call.args).to.have.length(1);
6773
const actualMsg = convertHttpResponse(call.args[0]);
6874

69-
let expectedMsg = expectedMsgs[i];
75+
let expectedMsg = filteredExpectedMsgs[i];
7076
if (expectedMsg instanceof RegExpStreamingMessage) {
7177
expectedMsg.validateRegExpProps(actualMsg);
7278
expectedMsg = expectedMsg.expectedMsg;

0 commit comments

Comments
 (0)