Skip to content

Cloud Functions

Wrap a handler in withLogging, and each entry it writes has functionName and the caller’s userId, which you did not pass.

This is a complete use of the package on its own: no browser client, no bundles, no source maps. The backend logger writes the same entry shape the browser does, so labels.userId="<uid>" returns both halves of a story in one query.

Call initLogger at module load in functions/src/index.ts. If you already set up the browser half, this is the same call.

functions/src/index.ts
import { initLogger } from '@dasasian/firebase-structured-logger/functions'
initLogger({ appId: 'my-app', logLocalDir: '../.fsl-logs' })

Import from /functions, not /client. The two halves have different initLogger functions, and the wrong one does not fail loudly. logLocalDir is for the emulator and is covered in Local development.

withLogging binds the request’s labels for the life of the handler:

functions/src/index.ts
import { onCall } from 'firebase-functions/v2/https'
import { withLogging, logInfo, logError } from '@dasasian/firebase-structured-logger/functions'
export const checkout = onCall(
withLogging({ functionName: 'checkout' }, async (request) => {
logInfo('started')
try {
await chargeCard(request.data)
} catch (err) {
logError(err, { orderId: request.data.orderId })
}
}),
)

logInfo('started') already has functionName and the caller’s userId. Labels passed to a single call, such as orderId above, merge with the request’s and win on a clash.

userId comes from the verified request.auth.uid. Do not pass it yourself. A schedule has no caller, so its entries have none.

onSchedule and onTaskDispatched handlers do not receive a CallableRequest. Name the event type as the second type argument, or tsc rejects the handler:

functions/src/index.ts
import { onSchedule, type ScheduledEvent } from 'firebase-functions/v2/scheduler'
import { onTaskDispatched, type Request } from 'firebase-functions/v2/tasks'
import { withLogging, logInfo } from '@dasasian/firebase-structured-logger/functions'
export const nightly = onSchedule(
'every day 02:00',
withLogging<Record<string, string | undefined>, ScheduledEvent>(
{ functionName: 'nightly' },
async () => {
logInfo('started')
},
),
)
type ReceiptJob = { orderId: string }
export const sendReceipt = onTaskDispatched(
withLogging<Record<string, string | undefined>, Request<ReceiptJob>>(
(request) => ({ functionName: 'sendReceipt', labels: { orderId: request.data.orderId } }),
async () => {
logInfo('sending')
},
),
)

The first type argument is your label type, described in Labels and filters. Pass Record<string, string | undefined> when you have none.

onRequest handlers take (req, res), and withLogging takes one argument. Leave them unwrapped. For an HTTP endpoint that should log browser entries, see Cloud Run and other Node servers.

Pass a function instead of an object and the labels are computed for each call:

functions/src/index.ts
import { onCall } from 'firebase-functions/v2/https'
import { withLogging, logInfo } from '@dasasian/firebase-structured-logger/functions'
type MyAppLabels = {
organizationId?: string
}
export const archive = onCall(
withLogging<MyAppLabels>(
(request) => ({ functionName: 'archive', labels: { organizationId: request.data.orgId } }),
async () => {
logInfo('started')
},
),
)

The label type has to be a type, not an interface. Labels and filters says why.

A request’s userId has to be gone when the request is. The easy way to hold it, AsyncLocalStorage.enterWith(), never unwinds. On a warm instance the next handler then inherits the previous caller, and every line it writes names the wrong person.

withLogging uses AsyncLocalStorage.run(), which restores the earlier context when the handler settles. Outside a handler, getLogger() returns an anonymous writer with no request labels, not whatever ran last.

Set minSeverity: 'INFO' on initLogger and deploy. Without it, a call that succeeds writes nothing, because the floor in production is WARNING. Call the function, then filter the Logs Explorer for labels.functionName="checkout". The entry should have userId without your having written it. If it does not, the handler is not wrapped, or the caller was not signed in.

createClientLogFunction, the callable that receives browser logs, is exported from the same entry point and is covered in Get started.

Made by Dasasian