Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

The basics

Logging

Oriel writes your app's log through channels: a file, a file per day, or stderr. Each entry is one line of JSON, written by pino.

Introduction#

Logging is organized in channels, as in Laravel. Each channel writes entries one way: the single channel appends them to one file, the daily channel to a file per day, and the stderr channel to the process's standard error. A stack channel sends each entry to several channels at once, and each channel keeps only the entries at or above its level.

The channels are written with pino, a Node logger, which writes each entry as a line of JSON.

Configuration#

The channels are configured in config/logging.ts. A new app's file:

config/logging.ts
import { dirname, join } from 'node:path'import { env } from '@orieljs/framework'const basePath = dirname(import.meta.dirname)const path = join(basePath, 'storage/logs/oriel.log')const level = env('LOG_LEVEL', 'debug')export default {  default: env('LOG_CHANNEL', 'stack'),  channels: {    stack: { driver: 'stack', channels: String(env('LOG_STACK', 'single')).split(',') },    single: { driver: 'single', path, level },    daily: { driver: 'daily', path, level, days: Number(env('LOG_DAILY_DAYS', 14)) },    stderr: { driver: 'stderr', level },    mail: { driver: 'single', path: join(basePath, 'storage/logs/mail.log'), level },    null: { driver: 'null' },  },}

default is the channel your entries go to: LOG_CHANNEL, stack unless you set it. The stack channel holds the channels LOG_STACK lists, comma-separated: single in a new app, which writes storage/logs/oriel.log. LOG_LEVEL sets the level of every channel, debug by default, so every entry is kept. The mail channel is there for the log mailer: with MAIL_LOG_CHANNEL=mail in your .env, the mail it would have sent goes to storage/logs/mail.log rather than to the default channel.

A channel name that channels doesn't define is an error when it is used: "Log channel [name] is not defined in config('logging.channels').".

Available channel drivers#

NameWhat it does
singleAppends every entry to the file path names, and creates its folder if needed
dailyWrites to one file per day, oriel-2026-10-19.log for oriel.log, and keeps the files of the last days days (14 by default): each time it starts a day's file, it deletes older ones
stderrWrites to the process's standard error, for a container or a process manager to collect
stackSends every entry to each of the channels it lists, in channels
nullDiscards every entry

Each channel but stack and null takes a level. A driver that isn't one of these is an error when its channel is used, "Log driver [name] is not registered.".

Building log stacks#

The stack driver combines channels, so one entry goes to all of them. Here the stack writes every entry to the daily files, and errors and worse to stderr too:

config/logging.ts
import { dirname, join } from 'node:path'import { env } from '@orieljs/framework'const basePath = dirname(import.meta.dirname)const path = join(basePath, 'storage/logs/oriel.log')export default {  default: env('LOG_CHANNEL', 'stack'),  channels: {    stack: { driver: 'stack', channels: ['daily', 'stderr'] },    daily: { driver: 'daily', path, level: 'debug', days: 14 },    stderr: { driver: 'stderr', level: 'error' },  },}

A stack has no level of its own: each of its channels keeps the entries that meet its own level. A stack that lists itself, directly or through another stack, is an error, "Log channel [stack] includes itself.".

Log levels#

A channel's level is the least severe level it keeps. Oriel has the eight levels of RFC 5424, from the most severe: emergency, alert, critical, error, warning, notice, info and debug. A channel at warning keeps warning, error, critical, alert and emergency entries, and drops the others:

Log.debug('An informational message.')Log.emergency('The system is down!')

With the stack above, the first entry is written to the daily file only, and the second to both channels. A level that isn't one of the eight, such as pino's warn, is an error when the channel is made: "Invalid log level [warn].".

Writing log messages#

The Log facade writes to the default channel, with a method for each level:

Log.emergency(message)Log.alert(message)Log.critical(message)Log.error(message)Log.warning(message)Log.notice(message)Log.info(message)Log.debug(message)

Log.log('warning', message) takes the level as its first argument. The logger() helper writes a debug entry, logger('Debug message'), and with no arguments returns the same log manager as the facade.

A controller logs as any other code does:

app/Http/Controllers/UserController.ts
import { Log, view } from '@orieljs/framework'import { User } from '../../Models/User'import { Controller } from './Controller'export class UserController extends Controller {  /** Show the profile for the given user. */  async show(id: string) {    Log.info('Showing the user profile for user', { id })    return view('user.profile', { user: await User.findOrFail(id) })  }}

Contextual information#

A method's second argument is the entry's context, an object written beside the message:

Log.info('User failed to login.', { id: user.id })

The message is written as it is. Laravel replaces placeholders such as {id} in the message with the context's values; Oriel keeps the context in its own fields, where log tools can search them. An Error in the context is written as its name, message and stack.

Log.withContext() adds context to every entry written after it in the same request, whatever the channel. A middleware can give each request an id:

app/Http/Middleware/AssignRequestId.ts
import { randomUUID } from 'node:crypto'import { Log, withHeaders } from '@orieljs/framework'import type { HttpRequest, Middleware, Next } from '@orieljs/framework/contracts'export class AssignRequestId implements Middleware {  async handle(request: HttpRequest, next: Next): Promise<Response> {    const requestId = randomUUID()    Log.withContext({ 'request-id': requestId })    const response = await next(request)    return withHeaders(response, (headers) => headers.set('Request-Id', requestId))  }}

The context belongs to the request: two requests running at once keep their own, and it is gone when the request ends. A console command and a queued job have a context of their own in the same way. Context added outside all of them, as in a service provider, reaches none of them.

Writing to specific channels#

Log.channel() writes to a channel other than the default one:

Log.channel('stderr').info('Something happened!')

Log.stack() makes a stack of channels for one entry:

Log.stack(['single', 'stderr']).info('Something happened!')

Laravel's Log::build(), which makes a channel from a configuration given at run time, isn't in Oriel: add the channel to config/logging.ts.

The log format#

Each entry is one line of JSON, as pino writes it. This is a line of storage/logs/oriel.log, which your app writes and you don't type, for the controller above:

{"level":30,"time":1792387488361,"pid":39221,"hostname":"web-1","id":"7","rfc5424Level":"info","msg":"Showing the user profile for user"}

msg is the message and rfc5424Level its level. time is in milliseconds since 1970, and pid and hostname tell the processes apart. The context's keys sit beside them, id here. level is pino's number for the level: 20 for debug, 30 for info and notice, 40 for warning, 50 for error, and 60 for critical, alert and emergency.

Reading the lines is up to the tools you use: tail -f storage/logs/oriel.log follows the file, and log services read JSON lines as they are. Laravel Pail, which tails the log in the terminal, isn't in Oriel.

Flushing the log#

pino writes in the background, so an entry may not be in the file yet when the line that logged it returns. Your app writes out every waiting entry, and closes the files, when it shuts down: node oriel after its command finishes, and the server when it receives SIGINT or SIGTERM (see Deployment). The log is closed after the database, queue and cache connections, so an entry logged while they close is kept.

A script that makes the app itself must shut it down before it exits, or the entries still waiting are lost:

scripts/report.ts
import { Log } from '@orieljs/framework'import { createApplication } from '../bootstrap/app'const app = await createApplication()Log.info('Report started')// ...await app.shutdown()

Calling process.exit() before shutdown() has finished loses them too. To write the waiting entries out without shutting the app down, await Log.flush(). A queued job or a command run by node oriel needs nothing: the command shuts the app down when it ends.

Monolog channel customization#

Oriel doesn't use Monolog, so Laravel's tap, Monolog handlers, formatters and processors, and channels made by your own factories, aren't in Oriel.

Privacy policy ·

esc