feat(api): add support for injecting langgraph_node in structured logs, expose structlog (#1444)

This commit is contained in:
David Duong
2025-07-26 00:56:29 +02:00
committed by GitHub
parent b1eb6ea0aa
commit 030698f473
8 changed files with 136 additions and 2 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@langchain/langgraph-api": patch
"@langchain/langgraph-sdk": patch
---
feat(api): add support for injecting `langgraph_node` in structured logs, expose structlog
+27 -1
View File
@@ -1,4 +1,4 @@
import { createLogger, format, transports } from "winston";
import { createLogger, format, transports, type Logger } from "winston";
import { logger as honoLogger } from "hono/logger";
import { consoleFormat } from "winston-console-format";
import type { MiddlewareHandler } from "hono";
@@ -10,9 +10,19 @@ import path from "node:path";
const LOG_JSON = process.env.LOG_JSON === "true";
const LOG_LEVEL = process.env.LOG_LEVEL || "debug";
let RUNTIME_LOG_FORMATTER:
| ((info: Record<string, unknown>) => Record<string, unknown>)
| undefined;
const applyRuntimeFormatter = format((info) => {
if (!RUNTIME_LOG_FORMATTER) return info;
return RUNTIME_LOG_FORMATTER(info) as typeof info;
});
export const logger = createLogger({
level: LOG_LEVEL,
format: format.combine(
applyRuntimeFormatter(),
format.errors({ stack: true }),
format.timestamp(),
format.json(),
@@ -20,6 +30,7 @@ export const logger = createLogger({
? [
format.colorize({ all: true }),
format.padLevels(),
consoleFormat({
showMeta: true,
metaStrip: ["timestamp"],
@@ -55,6 +66,21 @@ export const logger = createLogger({
transports: [new transports.Console()],
});
// Expose the logger to be consumed by `getLogger`
export function registerSdkLogger() {
const GLOBAL_LOGGER = Symbol.for("langgraph.api.sdk-logger");
type GLOBAL_LOGGER = typeof GLOBAL_LOGGER;
const maybeGlobal = globalThis as unknown as { [GLOBAL_LOGGER]: Logger };
maybeGlobal[GLOBAL_LOGGER] = logger;
}
export async function registerRuntimeLogFormatter(
formatter: (info: Record<string, unknown>) => Record<string, unknown>
) {
RUNTIME_LOG_FORMATTER = formatter;
}
const formatStack = (stack: string | undefined | null) => {
if (!stack) return stack;
+23 -1
View File
@@ -13,7 +13,12 @@ import { truncate, conn as opsConn } from "./storage/ops.mjs";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
import { queue } from "./queue.mjs";
import { logger, requestLogger } from "./logging.mjs";
import {
logger,
requestLogger,
registerRuntimeLogFormatter,
registerSdkLogger,
} from "./logging.mjs";
import { checkpointer } from "./storage/checkpoint.mjs";
import { store as graphStore } from "./storage/store.mjs";
import { auth } from "./auth/custom.mjs";
@@ -87,9 +92,26 @@ export async function startServer(options: z.infer<typeof StartServerSchema>) {
await Promise.all(callbacks.map((c) => c.flush()));
};
// Register global logger that can be consumed via SDK
// We need to do this before we load the graphs in-case the logger is obtained at top-level.
registerSdkLogger();
logger.info(`Registering graphs from ${options.cwd}`);
await registerFromEnv(options.graphs, { cwd: options.cwd });
// Make sure to register the runtime formatter after we've loaded the graphs
// to ensure that we're not loading `@langchain/langgraph` from different path.
const { getConfig } = await import("@langchain/langgraph");
registerRuntimeLogFormatter((info) => {
const config = getConfig();
if (config == null) return info;
const node = config.metadata?.["langgraph_node"];
if (node != null) info.langgraph_node = node;
return info;
});
const app = new Hono();
// Loopback fetch used by webhooks and custom routes
@@ -9,6 +9,9 @@ import {
} from "@langchain/langgraph";
import { BaseMessage, ToolMessage } from "@langchain/core/messages";
import { FakeListChatModel } from "@langchain/core/utils/testing";
import { getLogger } from "@langchain/langgraph-sdk/logging";
const logger = getLogger();
const getStableModel = (() => {
const cached: Record<string, FakeListChatModel> = {};
@@ -33,6 +36,8 @@ async function callModel(
): Promise<typeof AgentState.Update> {
let userId: string | undefined;
logger.warn("Hello, world!", { random: 123 });
if (config.configurable?.langgraph_auth_user != null) {
const user = config.configurable?.langgraph_auth_user as
| { identity: string }
+4
View File
@@ -14,6 +14,10 @@ react.cjs
react.js
react.d.ts
react.d.cts
logging.cjs
logging.js
logging.d.ts
logging.d.cts
react-ui.cjs
react-ui.js
react-ui.d.ts
+1
View File
@@ -16,6 +16,7 @@ export const config = {
client: "client",
auth: "auth/index",
react: "react/index",
logging: "logging/index",
"react-ui": "react-ui/index",
"react-ui/server": "react-ui/server/index",
},
+13
View File
@@ -100,6 +100,15 @@
"import": "./react.js",
"require": "./react.cjs"
},
"./logging": {
"types": {
"import": "./logging.d.ts",
"require": "./logging.d.cts",
"default": "./logging.d.ts"
},
"import": "./logging.js",
"require": "./logging.cjs"
},
"./react-ui": {
"types": {
"import": "./react-ui.d.ts",
@@ -138,6 +147,10 @@
"react.js",
"react.d.ts",
"react.d.cts",
"logging.cjs",
"logging.js",
"logging.d.ts",
"logging.d.cts",
"react-ui.cjs",
"react-ui.js",
"react-ui.d.ts",
+57
View File
@@ -0,0 +1,57 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
interface LeveledLogMethod {
(message: string, ...meta: any[]): Logger;
(message: any): Logger;
(infoObject: object): Logger;
}
interface Logger {
error: LeveledLogMethod;
warn: LeveledLogMethod;
help: LeveledLogMethod;
data: LeveledLogMethod;
info: LeveledLogMethod;
debug: LeveledLogMethod;
prompt: LeveledLogMethod;
http: LeveledLogMethod;
verbose: LeveledLogMethod;
input: LeveledLogMethod;
silly: LeveledLogMethod;
}
const GLOBAL_LOGGER = Symbol.for("langgraph.api.sdk-logger");
type GLOBAL_LOGGER = typeof GLOBAL_LOGGER;
/**
* Retrieves the global logger instance for LangGraph Platform.
*
* The logger provides structured logging capabilities with
* various log levels (error, warn, info, debug, etc.) and extra metadata such as node name etc.
*
* @returns {Logger} The global logger instance with leveled logging methods
*
* @throws {Error} When the logger is not available in the current environment
*
* @example
* ```typescript
* // Safe usage with fallback
* const logger = getLogger();
* logger.info("This will only work in LangGraph Platform environment");
* ```
*
* @remarks
* This method is designed to work specifically within the LangGraph Platform
* environment where a global logger is automatically registered. If you're
* developing locally or in an environment where LangGraph Platform is not
* available, this function will throw an error.
*/
export const getLogger = (): Logger => {
const maybeGlobal = globalThis as unknown as { [GLOBAL_LOGGER]: Logger };
if (GLOBAL_LOGGER in maybeGlobal) return maybeGlobal[GLOBAL_LOGGER];
throw new Error(
"Logger not available in current environment. " +
"This method requires LangGraph Platform environment where a global logger is automatically registered. " +
"If you're developing locally, consider using `console.log` or a local logging library instead."
);
};