Skip to main content

laravel-cloud-logging

Python logging for Laravel Cloud that matches a Laravel app's logs. Levels, context and exception chains show up in the Cloud dashboard the same way they do for Laravel. The package has no runtime dependencies. It supports Python 3.10 to 3.14.

from laravel_cloud_logging import configure

configure()

Call configure() once, as early as possible at startup. It:

  • replaces existing handlers on the root logger and on common framework loggers (uvicorn, gunicorn, celery, django, werkzeug, asyncio, rq.worker, py.warnings), and makes those loggers propagate to root;
  • captures warnings;
  • logs uncaught exceptions (main thread and threading) at CRITICAL;
  • silences uvicorn.access and gunicorn.access;
  • returns a logging.config.dictConfig dict, which Gunicorn can use.

You can call it more than once. Logging never raises into your app.

configure(level=None, *, exceptions=True, access_logs=False)
  • level: a name ("debug", "notice") or a number. Default: the LOG_LEVEL environment variable, then INFO. Unknown names fall back to INFO.
  • exceptions=False: do not install the uncaught-exception hooks.
  • access_logs=True: keep app-server access logs. They are off by default because Cloud's nginx already logs every request, with its status and timing.
pip install laravel-cloud-logging

Framework setup

wsgi_middleware and asgi_middleware are imported from laravel_cloud_logging.

Framework Setup
Plain script Call configure() before the first log call.
Flask Call configure() before you create the app. Then set app.wsgi_app = wsgi_middleware(app.wsgi_app).
FastAPI / Starlette Call configure() and app.add_middleware(asgi_middleware). Then start the server with uvicorn.run(app, log_config=None). If you run several uvicorn workers, also call configure() in the module that defines the app.
Django In settings.py, set LOGGING_CONFIG = None and call configure(). Add "laravel_cloud_logging.django.middleware" near the top of MIDDLEWARE. Under ASGI, you can wrap the app in asgi.py instead: application = asgi_middleware(get_asgi_application()).
Gunicorn In gunicorn.conf.py, set logconfig_dict = configure(). Do not set accesslog.
Celery Call from laravel_cloud_logging.celery import setup; setup(app). This sets worker_hijack_root_logger=False and connects configure() to the setup_logging signal with weak=False. Keyword arguments are passed to configure().
RQ Call configure() before or after the worker sets up its logging. Both orders work, because configure() also clears the handlers on rq.worker.
laravel-cloud-queues Call configure() before you start the worker. The worker calls basicConfig only when root has no handlers, so it keeps yours. Note: the worker's JSON job-event lines have no level or message today, so the dashboard shows them as plain entries.

Request IDs

The middleware reads the Cloud-Request-ID header into a contextvars.ContextVar (laravel_cloud_logging.cloud_request_id). Every record logged during the request then has context.cloud_request_id, which replaces any cloud_request_id you pass in extra=. The platform sets this header and replaces any value a client sends.

The package does not use X-Request-ID, because clients can set it and the platform passes it through. IDs longer than 128 characters are ignored.

  • WSGI and Django set the variable on every request, to None when the header is missing. That way a reused worker thread never keeps an old ID.
  • ASGI matches the header name in any case, handles only http and websocket scopes, and resets the variable when the request finishes.

Wire format

Each record is one compact JSON object on one line, in the Monolog shape that Laravel uses. The keys are always in this order:

Key Value
message record.getMessage()
context Always present ({} when empty). It holds every extra= field, plus cloud_request_id, exception and stack (stack_info).
level Monolog number (see below)
level_name Monolog name (see below)
channel APP_ENV, then LARAVEL_CLOUD_ENV_NAME, then local
datetime record.created as UTC ISO-8601 with microseconds, +00:00. For display only: the platform orders logs by the time it receives them.
extra {"logger": record.name}

Python levels map to Monolog levels by rounding down:

Python level level level_name
60 and above 600 EMERGENCY
55 550 ALERT
50 (CRITICAL) 500 CRITICAL
40 (ERROR) 400 ERROR
30 (WARNING) 300 WARNING
25 250 NOTICE
20 (INFO) 200 INFO
below 20 100 DEBUG

configure() registers NOTICE (25), ALERT (55) and EMERGENCY (60) as Python level names, but only when those numbers have no name yet. The constants are exported too: logger.log(laravel_cloud_logging.ALERT, "..."). The dashboard styles all eight names. The public logs API collapses them to info, warning, error and debug.

Exceptions go in context.exception as {class, message, code, file, trace, previous}:

  • class is module-qualified, without builtins..
  • code is args[0] when it is an int (not a bool). Otherwise it is 0.
  • file is path:line of the innermost frame.
  • trace holds up to 100 path:line in func strings, innermost first.
  • previous follows __cause__, or __context__ unless it is suppressed. It is recursive and safe against cycles.

The dashboard shows the whole chain.

Normalization follows Monolog's rules:

  • depth is limited to 9, and each container to 1000 items, using Monolog's marker strings;
  • non-finite floats become strings;
  • other objects become str(), and an object that cannot be printed becomes a marker.

Size cap: 256 KiB per line.

  1. First, the message and long top-level context strings are cut to 16 KiB each, with [truncated] added. The exception trace is cut to 20 frames, and previous is dropped.
  2. If the line is still too long, only exception, cloud_request_id and a truncated note are kept.
  3. If it is still too long, context keeps only the note, and channel and the logger name are cut to 16 KiB.

The 256 KiB budget includes the trailing newline. Cuts count UTF-8 bytes and never split a character.

The result is always valid JSON at the right level.

Why only these seven top-level keys

The platform picks the record type from top-level keys:

  • source: "nginx-app" makes the line a fake access log;
  • logger: "http.log.access.log0" makes it a Caddy access log;
  • _cloud_event takes the line out of the logs;
  • context selects the Laravel path.

So your extra= fields always go inside context, and can never reach the top level. The platform truncates records over 1 MB, and they become plain text at info level, so the 256 KiB cap keeps a large record structured. Evidence: SE-295 and its comments.

Transport and fallback

  • On Cloud (LARAVEL_CLOUD=1), lines go to LARAVEL_CLOUD_LOG_SOCKET. When that variable is not set, they go to unix:///tmp/cloud-init.sock. Python containers do not set the variable, but the socket exists. Supported addresses are unix://path, tcp://host:port and host:port.
  • Why a socket: every process in a Cloud container shares one stdout pipe. In a live test, 8 processes writing 60 KB lines to stdout corrupted 29 of 40 lines. Through the socket, all 40 lines arrived intact, because cloud-init writes one line at a time. See SE-301.
  • Connection:
    • one sendall per record, under the handler lock, with a 2 s timeout;
    • it connects on the first record;
    • it reconnects after fork(), so Gunicorn workers never share the parent's socket;
    • after a connect or send failure, it waits 5 s before it tries again.
  • Fallback: if the socket fails, or when you are not on Cloud, the whole line goes to sys.__stdout__ in one write, followed by a flush.
  • The platform splits socket lines over 2 MiB. The 256 KiB cap prevents this.

Limits

  • Anything printed before configure() runs is still plain text at info level. This includes interpreter crash output and server boot lines.
  • The dashboard cannot show whether a line came from stdout or stderr.
  • There is no redaction. Keep secrets out of messages and extra= fields.
  • Not in scope: Laravel's Exceptions feature (_cloud_event: exception), which is Laravel-only for now.

Development

uv run --python 3.14 --group test pytest -q

CI runs the tests on Python 3.10 to 3.14. The framework packages are test-only dependencies.

Live check on Laravel Cloud

  1. Run python scripts/live_check.py command <env>. It prints a cpx cloud command:run command and a marker. The command carries the package inside --cmd, so you do not need to deploy anything.

  2. Run the printed command. It prints the marker and a from/to window. command:run output itself is never logged, but lines sent to the socket are.

  3. Run python scripts/live_check.py verify <app> <env> <marker> <from> <to>. It checks:

    • every entry has type application, with the right levels;
    • there is exactly one exception entry, with its chain;
    • the request ID is present;
    • 40 of 40 concurrent lines arrived whole;
    • the 600 KB record is still JSON at warning level.

    The logs API returns at most 100 rows per call, so the script reads in small windows.

    The platform stores a logged exception as its own entry: type exception, with the exception's message as the entry message (not the log message), and class, code, file and trace as data. The logs API does not return previous, so check the chain in the dashboard.

  4. Check the dashboard Logs page by hand: the level tags and colours, and the exception chain in the details panel.

Run the check on a shared (Flex) environment and on a private one.

Releasing

Publishing uses PyPI trusted publishing (OIDC), so the repo stores no tokens. See .github/workflows/publish.yml.

  1. Set version in pyproject.toml, run uv lock, and merge to main.
  2. Publish to TestPyPI: run the Publish workflow manually on main (gh workflow run publish.yml --ref main).
  3. Publish to PyPI: create a GitHub release tagged v<version> (gh release create v0.0.1 --generate-notes). The pypi job waits for approval in the pypi environment.

License

MIT

Metadata

Release files for laravel-cloud-logging 0.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for laravel-cloud-logging 0.0.2
File Size Uploaded
laravel_cloud_logging-0.0.2.tar.gz 47.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for laravel-cloud-logging 0.0.2
File Interpreter ABI Platform
laravel_cloud_logging-0.0.2-py3-none-any.whl Python 3 none any Details

Total release size: 60.5 kB

Release files / laravel_cloud_logging-0.0.2.tar.gz

Download URL laravel_cloud_logging-0.0.2.tar.gz
Size 47.9 kB
Tags Source
SHA-256 checksum
How to use checksums
32fd6087fb75df85fc6741f65a8de98ff6d6f6b7afd0ab2d7d286da282bb02b9
BLAKE2b-256 checksum
How to use checksums
905cf0781d0fdaf708ba2840bffcfedf3041799d9852437f66a5a566ae0c2a6f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release files / laravel_cloud_logging-0.0.2-py3-none-any.whl

Download URL laravel_cloud_logging-0.0.2-py3-none-any.whl
Size 12.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d7c122bed72e6bc7aee6d58b3fe6203a366e1bce7d531700059003f33c4e3420
BLAKE2b-256 checksum
How to use checksums
e7ab0513bbae223cba8196136fe73404ab7256683e403299192e2068aec28a51
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.0

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

This release

0.0.2 This release

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page