Logging Configuration
Configuration section: [runtime.log]
Overview
The logging configuration controls the levels, where the records go, and how they are rendered. Where they go is one key, sink, naming a registered log sink; each shipped sink then reads its own settings from the section, and the rest of the section applies to every sink.
General Settings
Log Levels
default_log_level = "INFO"
- Sets the default logging level for all loggers
- Valid values:
"VERBOSE","DEBUG","INFO","WARNING","ERROR","CRITICAL","OFF"; what a line at each level means is in Logging "OFF"silences the loggers[runtime.log.package_log_levels]does not pin; Pipelex's own loggers are pinned there atINFO, so silencing them, whichever sink is selected, ispipelex = "OFF"in that section
Package-Specific Log Levels
[runtime.log.package_log_levels]
anthropic = "INFO"
asyncio = "INFO"
botocore = "INFO"
openai = "INFO"
pipelex = "INFO"
- Override log levels for specific packages
- The defaults keep the HTTP clients under the inference SDKs quiet:
httpx = "WARNING"andhttpcore = "INFO", with the same levels forhttpx2andhttpcore2, the forks openai 3.x sends its requests through, so an inference call prints no line of its own carrying the provider's endpoint URL - Use
-instead of.in package names (e.g.,urllib3-connectionpool) - A key works at any depth of the logger hierarchy, because Pipelex names every logger after the emitting module:
pipelexgoverns the whole runtime, andpipelex-pipe_operators-pipe_llm = "DEBUG"opens one module while the rest ofpipelexstays at its level
The Sink
sink = "console"
- Names the registered log sink boot installs on the root logger, the way
runtime.storage.methodnames a storage backend "console": the Rich handler, for a terminal. The default, so the CLI keeps its rendering. It needs Rich, which thecliextra (pipelex[cli]) declares, and a boot that selects it without Rich installed stops, naming the extra and thejsonalternative. The core dependenciestyperandinstructorinstall Rich anyway, so that refusal does not fire on an ordinary install: a server that forgot to selectjsonboots on this sink"json": one JSON object per line, for a server behind a log agent"otlp": the OpenTelemetry logs signal, for a collector"gcp": Google Cloud Logging through the client library, for a process that must write to it directly- Any other token an installed plugin registers; a token nobody provides stops the boot naming the registered ones
- The sink is a plugin capability: how it is discovered, selected and written is in Log Sink Plugins
Console Log Target
console_log_target = "stderr"
- The process stream the
consoleandjsonsinks write to:"stdout"or"stderr" - Default:
"stderr", because logs are diagnostics and must stay off the data channel console_print_target, beside it, is a different knob: the RichConsoleused for banners and the main CLI's data tables, which default to"stdout"so they can be piped to a file
Pretty-Print Mode
pretty_print_mode = "rich"
- Controls the panels that
pretty_print(...)renders, such as the "Output of pipe" panel shown after every operator pipe "rich": Rich tables and panels on the console print target. It needs Rich, which thecliextra (pipelex[cli]) declares, and a boot that selects it without Rich installed stops, naming the extra and the two modes below; as with theconsolesink, the core dependencies install Rich anyway, so that refusal does not fire on an ordinary install"poor": plain text in a drawn frame on stderr, with no Rich panel and no Rich import; a pipe's output prints as its plain rendering"silent": nothing is printed and no renderable is built, for a host with no console or one that must not spend time rendering on the thread that runs pipes- Default:
"rich". The agent CLI forces"silent", and a server sets"poor"or"silent"besidesink = "json"in its own configuration, since nothing at boot sets either for it - Boot applies the key, replacing any
PrettyPrinter.modeassigned in code before it, and teardown returns the printer to the mode the process held before that boot
JSON Formatting
json_logs_indent = 4
presentation_line_width = 120
json_logs_indent: Indentation of the JSON rendering a structured content (adict, alist, a model) gets in the messagepresentation_line_width: Maximum line width for formatted output
Caller Information
is_caller_info_enabled = false
caller_info_template = "file_line"
Available templates:
"file_line": "file.py:123""file_line_func": "file.py:123 function_name""func": "function_name""file_func": "file.py function_name""func_line": "function_name 123""func_module": "function_name module""func_module_line": "function_name module 123"
Redaction
[runtime.log.redaction]
is_enabled = true
extra_patterns = []
One processor, applied to every record before any sink renders it, whichever sink is selected. It does two things, and the difference between them matters.
It scrubs secrets from the message, from the exception's rendered text and from every string a field carries, replacing what a pattern matched with [REDACTED] and keeping whatever named the secret, so a reader still sees which header or which entry was carrying it. The shipped families are the ones the hosted plane already ran over its own records:
- an
Authorizationheader, whichever scheme it carries —Bearer,Basic,Token,Digest,Negotiate,HMAC— the scheme kept and the credential after it removed, - an api-key header or entry, however it is spelled (
x-api-key,api-key,api_key), - a webhook signature header (
x-signature,x-completion-signature), - an OAuth authorization code in a query string (
?code=…,&code=…), read to the&or the#that ends the parameter rather than through an alphabet of its own, so a percent-encoded code goes whole, - a
CookieorSet-Cookieheader's whole value, every crumb and every flag attribute of it. The value has to be a cookie header's ownname=valuegrammar, which is what tells a header from a sentence: a quoted crumb is read to its closing quote and a crumb holding an apostrophe is read whole, while prose that merely says "cookie:" before ordinary words keeps its line, - a serialised entry, in a JSON object or a Python
repralike, whose name is one of the secret names below: its value is removed whether it is quoted — with either quote character, and whichever quoted the name — or a bare number or boolean, - those same names again in the plain
name=valueorname: valueform a query string, a form-encoded body or a header line writes them in, which is the shape a raw request reaches the processor in, - a key recognisable by its prefix alone:
sk_,plx_sk_,pk_,bl_, each spelled with a dash as with an underscore, sosk-proj-…andsk-ant-api03-…go the waysk_live_…does. The prefix is kept and what follows it removed, and what follows has to end in a long unbroken run of letters and digits — which is what keeps this family off an ordinary snake_case identifier, a table or a partition namedpk_customer_reference_indexbeing words all the way down.
Every family but the serialised entry asks for at least eight characters of what a secret is written in. That bound is what keeps a family reading a name: value shape off ordinary prose, and it is the one the hosted plane's own scrubber was written with; a secret shorter than it is below what the shipped families read at all.
The secret names are password, pipelex_api_key, gateway_api_key, jwt_secret_key, portkey_api_key, client_secret, access_token, refresh_token, id_token, authorization, api_key, x_api_key, x_signature, x_completion_signature, cookie and set_cookie, read in any case and with a dash as an underscore. A field of the record whose own name is one of them loses its value whatever it holds — judged by the name the call used, so a field carried under the field_ collision prefix because the record already owned that name is covered too — and so does a mapping entry at any depth, since the mapping split the name from the value the families read together. code is deliberately not a secret name: it is the runtime's own identifier for a pipe, a domain and an error, and the OAuth code has its query-string family.
A structured value, a field's dict or list or the data a structured content produces, is walked to any depth, and its strings go through the families above. A pydantic model is dumped and any other object rendered as text before the walk, exactly as the json and otlp sinks would have done after it, so neither carries a secret past the scrub; a model or an object that refuses to render becomes an [UNRENDERABLE: <exception type>] marker, which costs that one value and leaves the record and every other value it carries. A container that contains itself is cut at the cycle with a [cycle] marker.
A structured content is redacted by name before the message is rendered, so the line a sink writes beside data says exactly what data holds: a secret nested in an object or held as a number is beyond what a pattern can read back out of text. That holds for content JSON refuses outright too — a circular reference, a mapping with a non-string key — which is rendered as its repr and carries no data: the names are redacted on that path as well, since a repr is no more readable to a pattern than a rendering is. Text a call rendered itself, an f-string of a dictionary for one, is covered by the serialised-entry family alone, which does not see into a nested object.
It neutralises control characters in a field's string values, and only there: each becomes its printable escape, so a newline in a caller-supplied value cannot forge a line and a tab cannot forge a field separator, and an escape byte cannot colour a terminal. The message and the data attribute keep their control characters, because they are the runtime's own rendering — a titled call and a structured content both put a newline there deliberately, and the wire sinks escape data themselves, so a logged prompt keeps its line breaks. data is the runtime's name alone: a call that passes a field of that name has it carried as field_data, and escaped like every other field.
It fails closed. A scrub that raises costs that record its processing, never the log call, and the record it hands on carries nothing of the call: the message becomes [REDACTION FAILED: <exception type>] and every field the record carried becomes [REDACTED]. Where even that stripping cannot complete — a stack that has run out is the usual reason the scrub failed in the first place, and it can run out again a line later — the record is dropped instead of emitted, so no sink ever receives one the scrub did not read.
is_enabled:trueby default.falseturns the processor off for a process that redacts downstream, or one whose records must be reproduced exactly as the calls made themextra_patterns: regular expressions added to the shipped families, for the secret shapes only this deployment knows. Every match is replaced by[REDACTED]. A pattern theremodule refuses is a configuration error named when the configuration loads, and so is one that matches the empty string —x*wherex+was meant matches at every position of every line, which would replace the whole of every line the process writes
What the processor does not reach is the console sink's Rich traceback, rendered from the exception object rather than from the scrubbed text, so on a person's terminal an exception reads as it was raised. Where the processor runs, and why it edits the record rather than a copy of it, is in Logging.
Problem Silencing
silenced_problem_ids = ["azure_openai_no_stream_options"]
- List of problem IDs to silence
- Prevents specific warnings from being logged
The console Sink
Configuration section: [runtime.log.rich_log], read only when sink = "console". The sink writes to console_log_target. It shows a record's fields after its message, coloured by name, and renders the layouts a call names; neither has a setting (see Console rendering).
Display Options
is_show_time = false
is_show_level = true
is_show_path = false
is_link_path_enabled = true
package_prefix = "libraries" # or "all", or "none"
is_show_time: Show timestamp in logsis_show_level: Show log levelis_show_path: Show the source file and line that logged each record, in a column at the right of the line. It is off by default: the column takes about 25 characters of every line and makes a long one wrapis_link_path_enabled: Make that path clickable, where the terminal supports linkspackage_prefix: Choose which lines start with their logger's top-level package name, dimmed, such ashttpx:(see Another library's lines)
package_prefix takes one of three values:
libraries, the default: a line another library logged starts with its package name, and a line frompipelexor from a package whose name starts withpipelex_, the API server'spipelex_apior a plugin named that way, has none. A plugin whose package is named otherwise counts as another libraryall: every line starts with its package name, Pipelex's own included, sopipelex:,pipelex_api:and a plugin's name tell the runtime, the API server and the plugin apartnone: no line has one
A line logged on the root logger names no package, so it never starts with one, whatever the value.
Syntax Highlighting
highlighter_name = "json" # or "repr"
highlighter_name: Choose between JSON or repr highlighting
The console reads no log message as Rich markup: a message prints exactly as written, and colour comes from the fields and the layouts (see Messages are plain text). There is no setting to turn markup on. A file that still sets is_markup_enabled does not stop the boot: Pipelex deletes the key in memory with ledger entry pipelex-config@7, writes nothing, and warns that the configuration is out of date. Run pipelex migrate to delete it from the file on disk, which also ends the warning.
Traceback Settings
is_rich_tracebacks = true
is_tracebacks_word_wrap = true
is_tracebacks_show_locals = false
tracebacks_suppress = []
- Control how Python tracebacks are displayed; an exception a call carries with
include_exception=Trueis rendered once, as a Rich traceback - Enable/disable word wrapping and local variable display
- Suppress specific traceback patterns
Keyword Highlighting
keywords_to_hilight = []
- List of keywords to highlight in log messages
- Useful for emphasizing important terms
The json Sink
No section of its own: the sink writes to console_log_target, one JSON object per line and no ANSI ever. Each line carries time (ISO 8601, UTC, milliseconds, Z), severity (the level name), logger (the module-named logger), message, exception when the record carries one, trace_id, span_id and trace_flags when OpenTelemetry's current span names a trace, pipelex.trace_id and pipelex.span_id when a Pipelex span is active, and then every field, the run-scoped identifiers and the data attribute flat beside them, under their own names. The three standard trace keys are the ones OpenTelemetry specifies for trace context in a JSON log that is not OTLP, lowercase hex at their full widths (32, 16 and 2 digits), and they name OpenTelemetry's current span, your own code's, which Pipelex reads and never changes, so a collector or an error tracker joins the line to your trace. The two pipelex.* keys, 32 and 16 hex digits, name the Pipelex span active at the log call, the pipe's own or the LLM call's during a run. The two sets are independent, so a line in a run under no span of yours carries only the pipelex.* keys (see Logging). Every key the sink writes, the trace keys included, is reserved on every line, so a field named like one of them is carried under a field_ prefix whether or not the line carries an exception or a span, and a non-finite float is written as the string "NaN", "Infinity" or "-Infinity". A value JSON refuses outright, a circular reference or a mapping with a non-string key, is written as its repr, and every other field on the same line keeps its JSON type, so a number or a boolean beside it stays queryable as one. The key names are the ones the CloudWatch agent, the Google Cloud Logging agent and any OTLP collector ingest without a parser.
{"time": "2026-09-16T10:12:03.417Z", "severity": "INFO", "logger": "pipelex.pipe_operators.pipe_llm", "message": "Running the pipe", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "span_id": "00f067aa0ba902b7", "trace_flags": "01", "pipelex.trace_id": "0af7651916cd43dd8448eb211c80319c", "pipelex.span_id": "b7ad6b7169203331", "request_id": "req-7f3a", "pipeline_run_id": "plr-01", "pipe_run_id": "pr-9c", "model": "gpt-5"}
The otlp Sink
Configuration section: [runtime.log.otlp], read only when sink = "otlp".
[runtime.log.otlp]
endpoint = "http://collector:4318/v1/logs"
headers = { Authorization = "Bearer ${OTLP_AUTH_TOKEN}" }
endpoint: The collector's logs URL. Left unset, the exporter follows the OpenTelemetry environment conventions:OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, thenOTEL_EXPORTER_OTLP_ENDPOINTwith the/v1/logspath, then the collector default on localhostheaders: Headers sent with every export, an authorization header typically. Empty, the default, leavesOTEL_EXPORTER_OTLP_HEADERSin charge. A value may name a secret rather than carry it, as the example's bearer token does, and the keys are taken as written: see Secrets in Sink Settings. This is the spellingtelemetry.tomluses for the trace exporter's headers, so the two OTLP exporters of one process name the same credential the same way- The records are exported in batches on the OTLP HTTP protocol, with the same service identity as the spans the runtime already exports, so a collector files the two together
- Each record carries the trace context of OpenTelemetry's current span, your own code's, so a collector files the record under your span; a record logged where no current span names a trace carries none. The Pipelex span active at the log call, the pipe's own or the LLM call's during a run, rides as the
pipelex.trace_idandpipelex.span_idattributes, in hex. Pipelex never makes its own spans current in the process's OpenTelemetry context, so your own instrumentation is never re-parented under them - The keys the sink writes itself — the source location
code.file.path,code.function.nameandcode.line.number,pipelex.trace_idandpipelex.span_id, and theexception.*keys — are reserved on every record whether or not it carries an exception or a Pipelex span, exactly as thejsonsink reserves its own keys: a field named like one of them is carried under the samefield_prefix, so nothing is overwritten and a field keeps one wire name whichever sink is selected - The sink never exports its own export path: a record the SDK or the transport emits while an export is in flight is rejected before the handler's lock is taken, so an unreachable collector costs a warning on the export thread and never a loop or a hang at exit. A flush at teardown is bounded by the exporter's own timeout,
OTEL_EXPORTER_OTLP_TIMEOUT, and whatever the sink raises while it flushes or closes is said on stderr rather than left to interrupt the teardown
The gcp Sink
Configuration section: [runtime.log.gcp], read only when sink = "gcp". The sink writes to Google Cloud Logging through the google-cloud-logging client library, which the gcp-logging extra installs: uv pip install "pipelex[gcp-logging]". Selecting the sink without the extra stops the boot with the install hint and the json alternative named.
Which of the two to pick
Most processes on Google Cloud should select json, not gcp. Cloud Run, GKE and every platform that runs a logging agent over the container's stdout ingest one JSON object per line with a severity and a message — exactly what the json sink writes — and the lines arrive in Cloud Logging with no client library installed, no credentials to hold and no API call on the logging path. Select gcp for the process the agent cannot serve: one with nothing ingesting its stdout, or one that must write to a log name or a project that is not the ambient one.
Settings
[runtime.log.gcp]
log_name = "pipelex"
project_id = "my-project"
credentials_file_path = "${GCP_CREDENTIALS_FILE_PATH}"
log_name: the Cloud Logging log the entries land under. Default:"pipelex"project_id: the project to write to. Left unset, the client library resolves it from the credentials or from the metadata server of the machine the process runs oncredentials_file_path: a service-account JSON file to build the client from. Left unset, authentication is Application Default Credentials, which is what a process already running on Google Cloud has. The path may be written as it is,"gcp_credentials.json", or name a secret, as the example does withGCP_CREDENTIALS_FILE_PATH, the secret thegcpstorage provider reads, so that one secret serves both: see Secrets in Sink Settings. When the path came from a placeholder, the credential errors below name the placeholder beside the resolved path, so you know to fix the secret rather than the TOML
Credentials are checked at boot
The sink refreshes its credentials once when it is built, before the transport starts, and waits no longer than five seconds for the answer. Over gRPC, the client library's default, a refresh that fails later is retried like a network error for a minute before anything reports it, so a short-lived process whose credentials no longer work would otherwise exit having lost every record, with nothing on stderr but the library's own count of what it could not send.
- Credentials Google refuses stop the boot: a refresh token that was revoked or has expired, a service-account key that was deleted or a key file that cannot be read, a machine on Google Cloud whose metadata server has no service account to hand out, or no credentials at all raise
GcpLogSinkCredentialsError, whose message names the credentials, quotes Google's answer and says what renews them:gcloud auth application-default loginon a workstation, the service account the machine runs as on Google Cloud, a valid key atcredentials_file_path, or thejsonsink instead.pipelex doctorreports the same message in its log-sink row - A refresh that gets no answer does not stop it: when the token endpoint or the metadata server cannot be reached, answers with a status that says to retry, or does not answer within the five seconds, nothing is known about the credentials themselves, so the sink says so on stderr and is installed, and its transport retries
- A service-account key over gRPC signs its own token and never calls the token endpoint, so for
credentials_file_paththe check costs one token exchange the transport would not have made; for Application Default Credentials that fetch a token, the token the check earns is the one the first export sends
What each entry carries
Each record becomes one Cloud Logging entry with a JSON payload:
- The level maps onto the Cloud Logging severity scale. That scale has nothing below
DEBUG, so Pipelex's customVERBOSElevel lands there - The payload carries
message,loggerandexceptionwhen the record carries one,pipelex.trace_idandpipelex.span_idwhen a Pipelex span is active, the pipe's own or the LLM call's, written exactly as thejsonsink writes them, then every field and thedataattribute flat beside them. The keys thejsonsink reserves are reserved here too,time,severity,trace_id,span_idandtrace_flagsincluded although the payload carries none of them — the client library takes the time and the severity out of band, and the entry's owntrace,spanIdandtraceSampledcarry the current span — so a field named like one of them is carried under afield_prefix under either sink rather than under one and not the other; a value JSON cannot carry — a non-finite float, a model, a circular structure — is written as text rather than costing the line - The run-scoped identifiers become the entry's labels rather than payload keys:
request_id,pipeline_run_idandpipe_run_id, whichever of them the record carries. Cloud Logging indexes labels, so these are what a query filters a run by - The entry's
trace,spanIdandtraceSampledname OpenTelemetry's current span, your own code's, the trace project-qualified asprojects/<project>/traces/<trace-id>, so Cloud Logging files the line under your trace. A line logged where no current span names a trace carries none of the three, inside a Pipelex run too: thepipeline_run_idlabel is what selects a run's lines, and thepipelex.*payload keys name its spans - The entries leave through the client library's background-thread transport, which batches them off the thread that logged, so no record costs an API round trip on the calling thread. The teardown flushes and closes it, and the flush carries a deadline of its own, five seconds, because the library's does not. A flush that runs out of time says so on stderr, that the transport was still writing and that what it still holds is lost unless it sends it while it closes, followed by what a fresh refresh of the credentials answers, a refresh given five seconds of its own, so such a flush returns within ten and the close then gives the transport the client library's own grace period: refused, with Google's answer, which is why the write is held; unreachable; or still accepted, in which case the Cloud Logging API or the network is what holds it. A handler that has closed does not flush again when the process exits
- What the export path logs never leaves through the sink: the library reports a refused batch through a logger of its own, and a report exported through the pipeline it reports on would fail with it and be reported again. The handler rejects those records, and every record emitted on the library's export thread, before its lock is taken — the same guard the
otlpsink carries, against the same deadlock at exit. What the sink's own credentials check logs while a teardown's flush waits on it is rejected the same way and printed on stderr, so the check still answers when the process's exit holds the handler's lock - A refused write is still said: the library marks a refused batch done, so the flush succeeds and nothing else would report it, and the entries in it are lost. The export path's warnings and errors are printed on stderr instead, the first one in each window of time with its traceback and the ones after it counted in a single line once that window has passed — with the next failure, on the next line the process logs, or at the teardown at the latest — so a process whose writes Cloud Logging refuses says so without printing once per line it logs
Secrets in Sink Settings
The values of the otlp sink's headers and the gcp sink's credentials_file_path accept the ${…} placeholders the inference backends use, resolved through the secrets provider [runtime.secrets] selects. Boot builds that provider before it installs the sink for this reason, and hands it to the sink's factory.
${NAME}and${secret:NAME}readNAMEfrom the secrets provider, which with the defaultenvmethod is the environment variable of that name${env:NAME}reads the environment variable whatever the secrets method${env:NAME|secret:OTHER}tries each in turn- A placeholder can sit inside a longer value,
"Bearer ${OTLP_AUTH_TOKEN}", and the rest of the value is kept as written
The configuration keeps the placeholders, so nothing that prints the configuration can print a credential. A header's resolved value reaches the exporter alone, and the resolved key path reaches the Cloud Logging client and the gcp sink's credential errors, which quote it beside its placeholder. A placeholder that does not resolve stops the boot with LogSinkVariableError, whose message names the section, the key and the variable, never a value, rather than sending the placeholder to the collector as if it were the token and losing every record while the boot reports success; pipelex doctor reports the same message in its log-sink row. A header value that holds a line break once resolved stops the boot with LogSinkHeaderValueError, naming the key and never the value: no HTTP header can carry one, and the exporter would otherwise be installed and refuse every batch. A secret read from a file often ends with a newline, so store it without one. There is no escape syntax, so a header value that genuinely contains ${ cannot be written. A secrets provider that fails to build stops the boot before any sink exists, and the lines logged until then are written to stderr, redacted, as they are for any boot that dies before its sink. pipelex doctor quotes that failure in its Secrets Provider row, and a sink's failure in its log-sink row, scrubbed with the same redaction patterns, the configured extra_patterns included.
Example Configuration
[runtime.log]
default_log_level = "INFO"
sink = "console"
console_log_target = "stderr"
console_print_target = "stdout"
pretty_print_mode = "rich"
json_logs_indent = 4
presentation_line_width = 120
is_caller_info_enabled = true
caller_info_template = "file_line_func"
silenced_problem_ids = []
[runtime.log.redaction]
is_enabled = true
extra_patterns = ["tnt-[0-9]+"]
[runtime.log.package_log_levels]
pipelex = "INFO"
openai = "WARNING"
anthropic = "INFO"
[runtime.log.rich_log]
is_show_time = false
is_show_level = true
is_show_path = false
is_link_path_enabled = true
highlighter_name = "json"
is_rich_tracebacks = true
is_tracebacks_word_wrap = true
is_tracebacks_show_locals = false
tracebacks_suppress = []
keywords_to_hilight = ["error", "warning", "failed"]
package_prefix = "libraries"
[runtime.log.otlp]
headers = {}
[runtime.log.gcp]
log_name = "pipelex"
Migrating From the Log Mode
log_mode (rich / poor), poor_loggers, generic_poor_logger and is_console_logging_enabled are gone from the schema, and the defaults layer supplies sink = "console" in their place. Run pipelex migrate: ledger entry pipelex-config@5 deletes the four keys from an existing file, and the defaults then take over.
Two of those keys chose a behaviour their deletion undoes, and no operation in the migration vocabulary can write the replacement — so where a choice was made, writing the replacement is yours:
is_console_logging_enabled = falsesuppressed every record Pipelex's own log calls emitted. Its equivalent ispipelex = "OFF"under[runtime.log.package_log_levels], which deep-merges over the base'sINFOand leaves the third-party levels alone;default_log_levelgoverns only the loggers that section does not pin, so on its own it silences none of Pipelex's records. Silence for everything takesdefault_log_level = "OFF"and every entry of that section atOFF, and sending the records elsewhere instead means selecting the sink that goes there.is_console_logging_enabled = truewas the default and asks for nothing: the migration deletes it and the new default renders the same console.log_mode = "poor"chose a plain handler for a process with no terminal; that process now setssink = "json".log_mode = "rich"chose whatconsolerenders, and asks for nothing either.poor_loggersandgeneric_poor_loggerhave nothing to carry over: every record goes to the one selected sink.
Migrating From the DEV Level
The DEV level, which sat between DEBUG and INFO, is gone, and so is log.dev(). Ledger entry pipelex-config@6 reads DEV as INFO wherever a file names it, as default_log_level or as a level under [runtime.log.package_log_levels]. INFO is the equivalent because a DEV threshold let through INFO and above plus the lines logged at DEV itself, and since nothing logs at DEV any more, INFO lets through exactly the same lines. A third-party package you held at DEV therefore keeps its debug output suppressed, a provider SDK's request dumps included. A file that still names DEV does not stop the boot: Pipelex migrates it in memory, starts at INFO, writes nothing, and warns that the configuration is out of date. Run pipelex migrate to rewrite the file on disk, which also ends the warning. Set DEBUG yourself where you want the lines a diagnosis needs.
Best Practices
-
Development Environment:
- Keep the
consolesink, and enable caller info for better debugging - Use verbose logging levels
- Enable local variables in tracebacks
- Keep the
-
Production Environment:
- Prefer a sink that holds no credential: the
jsonsink on stderr, shipped by a collector or by the platform's log agent, which authenticates on the process's behalf with the platform's own identity. Nothing in the process then holds a logging credential, and a misconfigured one cannot lose its records - Select the
otlpsink in front of a collector that needs a token, or thegcpsink for a process that must write to Google Cloud Logging directly, only when no agent can ship the lines; their credentials then belong in the secrets provider, named in their settings rather than written there - Disable caller info for performance
- Use INFO or higher log levels
- Prefer a sink that holds no credential: the
-
Package Log Levels:
- Set noisy third-party packages to WARNING
- Keep pipelex at INFO for important updates
- Use VERBOSE only when debugging specific issues
Related Documentation
- Logging Tool - Using Pipelex logging in your code
- Log Sink Plugins - The seam behind the
sinkkey, and how to write a sink