mirror of
https://github.com/langchain-ai/langgraphjs.git
synced 2026-07-22 17:15:25 -04:00
feat(api): add support for injecting langgraph_node in structured logs, expose structlog (#1444)
This commit is contained in:
@@ -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
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user