-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Restructuring log middleware #10
Changes from all commits
d035515
0a39991
2f5e462
3e1e407
c4bfabd
cfb3998
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
coverage | ||
dist | ||
node_modules | ||
.eslintcache |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,4 @@ | ||
export { fetchGcpProjectId } from "./lib/gcp"; | ||
export { getHttpTraceHeader } from "./lib/http"; | ||
export { getLoggingTraceData, logger } from "./lib/logging"; | ||
export { decorateLogs, logger, Logger, LoggerOptions, DestinationStream } from "./lib/logging"; | ||
export { middleware } from "./lib/middleware"; | ||
export { createTraceparent } from "./lib/traceparent"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,17 @@ | ||
import gcpMetaData from "gcp-metadata"; | ||
|
||
let gcpProjectId: string | undefined = undefined; | ||
|
||
export function getGcpProjectId() { | ||
return process.env.GCP_PROJECT || gcpProjectId; | ||
} | ||
|
||
/** | ||
* Fetches the Google Cloud Platform (GCP) project ID from the GCP metadata server. | ||
* | ||
* You only need to call this function if you're not setting the `GCP_PROJECT` environment variable. | ||
* You can alternatively set the `GCP_PROJECT` environment variable, which takes precedence. | ||
*/ | ||
export async function fetchGcpProjectId() { | ||
export async function getGcpProjectId(): Promise<string | undefined> { | ||
if (process.env.GCP_PROJECT) { | ||
return process.env.GCP_PROJECT; | ||
} | ||
|
||
const isAvailable = await gcpMetaData.isAvailable(); | ||
if (!isAvailable) return; | ||
|
||
gcpProjectId = await gcpMetaData.project("project-id"); | ||
} | ||
|
||
/** | ||
* Resets the GCP project ID, for testing. | ||
*/ | ||
export function reset() { | ||
gcpProjectId = undefined; | ||
return await gcpMetaData.project("project-id"); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
import { getStore } from "./middleware"; | ||
|
||
export function getHttpTraceHeader() { | ||
const { traceparent } = getStore(); | ||
if (traceparent) return { traceparent }; | ||
return {}; | ||
export function getHttpTraceHeader(): Record<string, string> { | ||
const { traceparent } = getStore() || {}; | ||
return traceparent ? { traceparent } : {}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,57 @@ | ||
import type { RequestHandler } from "express"; | ||
import { AsyncLocalStorage } from "node:async_hooks"; | ||
import { createTraceparent } from "./traceparent"; | ||
import { getGcpProjectId } from "./gcp"; | ||
import { getTraceFromTraceparent, createTraceparent } from "./traceparent"; | ||
|
||
type Store = { | ||
traceparent?: string; | ||
clientServiceAccount?: string; | ||
type LogFields = { | ||
traceId?: string; | ||
spanId?: string; | ||
"logging.googleapis.com/trace"?: string; | ||
"logging.googleapis.com/spanId"?: string; | ||
"logging.googleapis.com/trace_sampled"?: boolean; | ||
[key: string]: unknown; | ||
}; | ||
|
||
export type Store = { | ||
traceparent?: string; | ||
logFields: LogFields; | ||
}; | ||
|
||
export type Middleware = () => RequestHandler; | ||
|
||
const storage = new AsyncLocalStorage<Store>(); | ||
|
||
export const middleware: RequestHandler = (req, _res, next) => { | ||
const traceparent = req.header("traceparent") || createTraceparent(); | ||
export const middleware: Middleware = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps also rename this to something better too. Prefix with create? Or should it be called something less generic over all - There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perfect! I've always felt torn on this topic, since named exports affect implementors this way. But I'm happy with this approach. |
||
let initialized = false; | ||
let projectId: string | undefined; | ||
|
||
return async (req, _res, next) => { | ||
if (!initialized) { | ||
initialized = true; | ||
projectId = await getGcpProjectId(); | ||
} | ||
|
||
const traceparent = req.header("traceparent") || createTraceparent(); | ||
const trace = getTraceFromTraceparent(traceparent); | ||
Comment on lines
+34
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should combine these rows, since we might get no There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think both variants work. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll leave it like this for now then 😄 |
||
const logFields: LogFields = {}; | ||
|
||
if (trace) { | ||
logFields.traceId = trace.traceId; | ||
logFields.spanId = trace.parentId; | ||
|
||
if (projectId) { | ||
logFields["logging.googleapis.com/trace"] = `projects/${projectId}/traces/${trace.traceId}`; | ||
logFields["logging.googleapis.com/spanId"] = trace.parentId; | ||
logFields["logging.googleapis.com/trace_sampled"] = trace.isSampled; | ||
} | ||
} | ||
|
||
storage.run({ traceparent }, () => { | ||
next(); | ||
}); | ||
storage.run({ traceparent, logFields }, () => { | ||
next(); | ||
}); | ||
}; | ||
}; | ||
|
||
export function getStore() { | ||
return storage.getStore() || {}; | ||
export function getStore(): Store | undefined { | ||
return storage.getStore(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure we need this as a separate function. Also, in this form, you can only add static data to your logs, which might not be that interesting. My thought was to add an optional argument to the
middleware
factory function, that can add logging data from theRequest
object. something like:This way, you also don't have to keep track of whether the storage has been initalized or not, it's all done in the middleware.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My specific use case in greenfield is extracting the IAP headers to log which service account made a specific request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that type of middleware option function is great! But I think its nice to have a separate
decorateLogs
type function that you can use anywhere in your application, such asif (user) { decorateLogs(...) }
. Lets att that middleware option in the next PR 🙏I can remove the extra function 👌
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait, I misunderstood. I thought you were referring to another function. I think separating the argument on the middleware from decorateLogs() could be nice, since they cover different use cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, so the idea is to write my own middleware where
decorateLogs
is called? That is probably even more flexible actually.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you can either call it inside your own middleware, or anywhere in the request flow. I have used it on Di's site in some conditional logic before.