🎉
Hono + Microsoft Azure FunctionsでContextのLogを出力してみたメモ
概要
前回にAzure FunctionsでHonoを動かした。
今回はログを開始時と終了時に出そうと考えている。
Hono Azure Functionsで紹介されている方法だとcontext.logが使えないので対応を考えた。
context.logを何故使いたいのか
ログを出すだけなら、console.logでも可能。
MSのドキュメントではcontext.logが推奨されている。
そこで、ログの出力にどんな差がでるのか試してみる。
applications.insightsのtraceログで出力される項目を確認対象とした。
結果、context.logのほうが出力される項目が多いことが分かった。
特にoperation_Id
の有無がトレーサビリティに関わると思われる。
共通で出力される項目
項目 | 例 |
---|---|
timestamp [UTC] | 2024/8/24 17:13:09.793 |
message | Hello World |
severityLevel | 1 |
itemType | trace |
client_Type | PC |
client_IP | 0.0.0.0 |
cloud_RoleName | <Function名> |
cloud_RoleInstance | XXXXXX-XXXXXXXXXXXXX |
appId | <UUID> |
appName | /subscriptions/<サブスクリプションID>/resourcegroups/<リソースグループ名>/providers/microsoft.insights/components/<インサイト名> |
iKey | <UUID> |
sdkVersion | azurefunctions: 4.34.2.2 |
itemId | <UUID> |
itemCount | 1 |
_ResourceId | /subscriptions/<サブスクリプションID>/resourcegroups/<リソースグループ名>/providers/microsoft.insights/components/<インサイト名> |
context.logのみに出力される項目
項目 | 例 |
---|---|
operation_Name | httpTrigger |
operation_Id | hogehoge |
operation_ParentId | piyopiyo |
client_City | Sibuya |
client_StateOrProvince | Tokyo |
client_CountryOrRegion | Japan |
出力が違う項目
項目 | 例 |
---|---|
customDimensions | JSON |
項目 | console.log | context.log |
---|---|---|
LogLevel | Information | Information |
HostInstanceId | moga | moga |
ProcessId | 61 | 61 |
Category | Host.Function.Console | Function.httpTrigger.User |
prop__{OriginalFormat} | Hello World | - |
prop__MS_AzureFunctionsRequestID | - | hogehoge |
InvocationId | - | hogepiyo |
Honoでcontext.logを使用する
Honoのenv.bindingsの機能を利用し、env.AZURE_FUNCTIONS_CONTEXT
にFunctionsのcontextを格納する。
Middlewareとして開始と終了のログを出すことにした。
apps/api/src/functions/httpTrigger.ts
import { app, HttpResponseInit } from '@azure/functions';
import honoApp from '@api/app';
import { HttpRequest, InvocationContext } from '@azure/functions';
import type { ReadableStream } from 'node:stream/web';
import { ExecutionContext } from 'hono';
const newAzureFunctionsResponse = (response: Response): HttpResponseInit => {
const returnBase = {
status: response.status,
headers: headersToObject(response.headers),
};
if (!response.body) return returnBase;
return { ...returnBase, body: streamToAsyncIterator(response.body) };
};
const streamToAsyncIterator = (readable: ReadableStream<Uint8Array>) => {
const reader = readable.getReader();
return {
next() {
return reader.read();
},
return() {
return reader.releaseLock();
},
[Symbol.asyncIterator]() {
return this;
},
} as AsyncIterableIterator<Uint8Array>;
};
type LoopableHeader = {
forEach: (callbackfn: (value: string, key: string) => void) => void;
};
function headersToObject(input: LoopableHeader): Record<string, string> {
const headers: Record<string, string> = {};
input.forEach((v, k) => (headers[k] = v));
return headers;
}
const newRequestFromAzureFunctions = (request: HttpRequest): Request => {
const hasBody = !['GET', 'HEAD'].includes(request.method);
return new Request(request.url, {
method: request.method,
headers: headersToObject(request.headers),
...(hasBody ? { body: request.body, duplex: 'half' } : {}),
});
};
type FetchCallback = (
request: Request,
env: Record<string, unknown>,
executionCtx?: ExecutionContext,
) => Promise<Response> | Response;
function azureHonoHandler(fetch: FetchCallback) {
return async (request: HttpRequest, _context: InvocationContext) => {
return newAzureFunctionsResponse(
await fetch(newRequestFromAzureFunctions(request), {
...process.env,
AZURE_FUNCTIONS_CONTEXT: _context,
}),
);
};
}
app.setup({
enableHttpStream: true,
});
app.http('httpTrigger', {
methods: ['GET', 'POST', 'PUT', 'DELETE'],
authLevel: 'anonymous',
route: 'api/{*proxy}',
// 第2引数のcontextはenv.AZURE_FUNCTIONS_CONTEXTに格納する
handler: azureHonoHandler(honoApp.fetch),
});
apps/api/src/app.ts
import characters from './routes/characters';
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import transactionTest from './routes/transactionTest';
import { MiddlewareHandler } from 'hono/types';
import { AppContext } from './types';
const route = new Hono<AppContext>()
.route('/characters', characters)
.route('/test', transactionTest)
// loggerの使い方サンプル
.get('/echo', async (c) => {
const { logger } = c.get('services');
// ログ出力の順番確認用に少し待つ
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
const test = process.env.HOGE;
logger.log(`echo: ${test}`);
return c.json({ echo: test });
});
;
const startEndLogMiddleWare: MiddlewareHandler<AppContext> = async (
c,
next,
) => {
const logger = new Logger(c.env.AZURE_FUNCTIONS_CONTEXT);
c.set('services', { logger });
logger.log(`func start: ${c.req.url}`);
await next();
logger.log(`func end: ${c.req.url}`);
};
const app = new Hono<AppContext>()
.use(startEndLogMiddleWare)
.use(
'/api',
cors({
origin: '*',
allowHeaders: ['Content-Type'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
}),
)
.route('/api', route);
export default app;
export type AppType = typeof app;
apps/api/src/types.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
import { InvocationContext } from '@azure/functions';
type Bindings = {
AZURE_FUNCTIONS_CONTEXT: InvocationContext;
};
type Logger = {
log(...args: any[]): void;
trace(...args: any[]): void;
debug(...args: any[]): void;
info(...args: any[]): void;
warn(...args: any[]): void;
error(...args: any[]): void;
};
export type AppContext = {
Bindings: Bindings;
Variables: {
services: {
logger: Logger;
};
};
};
参考
Discussion