|
For Canonical Logs (a single log per request - https://www.structlog.org/en/stable/logging-best-practices.html#canonical-log-lines), I would write something like this in Python with structlog:
However, I'm not sure how to do that with LogTape or what pattern to follow. Adapting the example from: https://logtape.org/manual/contexts#basic-usage, when function A doesn't log and the log happens in the middleware, the context isn't captured import { getLogger, withContext } from "@logtape/logtape";
function functionA() {
// getLogger("a").info("This log message will have the implicit context: {requestId}.");
}
function handleRequest(requestId: string) {
withContext({ requestId }, () => {
functionA();
});
// How do I have implicit context during the life of the request and not just within the function?
getLogger("a").info("This log message does not have the implicit context: {requestId}.");
}Any advice would be appreciated! Maybe I'm missing something or maybe this would need to be a new feature |
Replies: 2 comments 1 reply
|
With a bit of GenAI, I have a working example of how this should work, which might just require extending the logger with a method for import { AsyncLocalStorage } from "node:async_hooks"
const asyncLocalStorage = new AsyncLocalStorage()
function getLogger() {
const store = asyncLocalStorage.getStore() || {}
return {
getContext() {
return { ...store.logContext }
},
bindContext(newContext) {
const updatedContext = { ...store.logContext, ...newContext }
store.logContext = updatedContext
return this
},
info(message) {
const contextStr = Object.entries(store.logContext || {})
.map(([k, v]) => `${k}=${v}`)
.join(" ")
console.log(`[INFO] ${contextStr} ${message}`)
},
error(message) {
const contextStr = Object.entries(store.logContext || {})
.map(([k, v]) => `${k}=${v}`)
.join(" ")
console.log(`[ERROR] ${contextStr} ${message}`)
},
}
}
function initializeContext(callback) {
asyncLocalStorage.run({ logContext: {} }, callback)
}
// ----
function functionA() {
const logger = getLogger()
logger.bindContext({ a: 1 })
functionB()
}
function functionB() {
const logger = getLogger()
logger.bindContext({ b: 2 })
}
initializeContext(() => {
functionA()
const logger = getLogger()
logger.info("Request Complete") // [INFO] a=1 b=2 Request Complete
}) |
|
Hi @KyleKing! LogTape already supports exactly what you're looking for with implicit contexts. The issue in your example is likely that you haven't configured The Missing ConfigurationTo enable implicit contexts, you need to set import { AsyncLocalStorage } from "node:async_hooks";
import { configure } from "@logtape/logtape";
await configure({
// ... your other settings ...
contextLocalStorage: new AsyncLocalStorage(),
});Without this configuration, Fixed ExampleOnce you add the configuration, your original example should work correctly: import { getLogger, withContext } from "@logtape/logtape";
function functionA() {
// This will now have implicit context!
getLogger("a").info("This log message will have the implicit context: {requestId}.");
}
function handleRequest(requestId: string) {
withContext({ requestId }, () => {
functionA();
// This will also have implicit context since it's inside the withContext callback
getLogger("a").info("This log message DOES have the implicit context: {requestId}.");
});
}The key issue in your original example was that the final log call was outside the Canonical Logs PatternFor canonical logs with middleware, you can structure it like this: function loggingMiddleware(req, res, next) {
const requestId = generateRequestId();
const startTime = Date.now();
withContext({ requestId, startTime }, () => {
next();
// Canonical log at the end of request
const duration = Date.now() - startTime;
const logger = getLogger("middleware");
if (res.statusCode >= 500) {
logger.error("Request completed", {
status: res.statusCode,
duration,
// requestId is automatically included via implicit context
});
} else if (res.statusCode >= 400) {
logger.warn("Request completed", { status: res.statusCode, duration });
} else {
logger.info("Request completed", { status: res.statusCode, duration });
}
});
}Adding Context IncrementallyIf you need to add context throughout the request lifecycle, you can use nested withContext({ requestId }, () => {
// ... some logic ...
withContext({ userId: getCurrentUser() }, () => {
// Now has both requestId and userId
withContext({ operation: 'userUpdate' }, () => {
// Now has requestId + userId + operation
logger.info("Operation completed"); // All context included
});
});
});Or chain explicit contexts: const baseLogger = getLogger("app");
let currentLogger = baseLogger.with({ requestId });
// Later when you need more context
currentLogger = currentLogger.with({ userId });
currentLogger = currentLogger.with({ operation });LogTape's approach with immutable contexts and functional composition is more predictable and safer than mutable global context binding. See the contexts documentation for more details. |
Hi @KyleKing!
LogTape already supports exactly what you're looking for with implicit contexts. The issue in your example is likely that you haven't configured
contextLocalStoragein your LogTape setup.The Missing Configuration
To enable implicit contexts, you need to set
contextLocalStoragein yourconfigure()call:Without this configuration,
withContext()won't inject implicit contexts into log messages, and LogTape will log a warning to the meta logger about the missing option.Fixed …