Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Betterstack Logs Laravel Package

goedemiddag/betterstack-logs

Laravel package for integrating Better Stack Logs with your application. It helps route Laravel logs to Better Stack for centralized collection, monitoring, and debugging, making production logs easier to search and manage.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up in five minutes.

composer require goedemiddag/betterstack-logs
BETTERSTACK_SOURCE_TOKEN=your-source-token
LOG_CHANNEL=stack

Add a channel in config/logging.php next to your existing ones and compose it into the stack:

'channels' => [
    'stack' => [
        'driver'   => 'stack',
        'channels' => ['daily', 'betterstack'],
    ],

    'betterstack' => [
        'driver' => 'betterstack',
        'token'  => env('BETTERSTACK_SOURCE_TOKEN'),
        'level'  => env('LOG_LEVEL', 'debug'),
    ],
],
php artisan config:clear
php artisan tinker --execute="Log::info('betterstack smoke test', ['env' => app()->environment()]);"

Then confirm the record actually lands in the Better Stack live-tail view. Don't skip this — a wrong token fails silently (see Gotchas), so "the code ran without an exception" proves nothing.

Where to look first.

  • vendor/goedemiddag/betterstack-logs/ — the service provider is small; read it once to confirm the exact driver name, the accepted config keys, and whether the handler is synchronous or buffered. This five-minute read answers most questions the README doesn't.
  • config/logging.php — your only real integration surface. Nothing else in the app changes.
  • The Better Stack UI — this becomes your primary debugging tool once shipped. Learn the query syntax early.
  • composer.json of the package — check Monolog and Laravel constraints before you plan a framework upgrade.

First real use case: route production and staging warning+ to Better Stack while keeping daily as a durable local sink, and leave local development on single. You get centralized search and alerting in prod, full-fidelity files on disk, and zero network chatter from your laptop.


Implementation Patterns

Always stack, never replace. Better Stack is an observability tool, not a system of record. Keeping daily in the stack means an ingestion outage is a loss of convenience, not a loss of logs, and it gives you a local fallback to diff against when something "doesn't show up."

Environment-aware channel lists. Build the stack conditionally rather than switching LOG_CHANNEL wholesale:

'stack' => [
    'driver'   => 'stack',
    'channels' => app()->environment('local', 'testing')
        ? ['daily']
        : ['daily', 'betterstack'],
],

Leave LOG_CHANNEL=stack in .env.example so every environment follows the same code path and only the channel list varies.

Structured context instead of string interpolation. This is the single highest-leverage habit with any hosted log service:

// Queryable fields in Better Stack
Log::warning('Payment retry scheduled', [
    'order_id'     => $order->id,
    'tenant_id'    => $order->tenant_id,
    'attempt'      => $attempt,
    'gateway'      => $gateway->name,
    'duration_ms'  => $elapsed,
]);

// Searchable only by luck
Log::warning("Order {$order->id} retry #{$attempt} on {$gateway->name}");

Agree on a small vocabulary of keys (tenant_id, order_id, request_id, job, duration_ms) and enforce it in review. Consistency is what makes alerts and saved queries reusable.

Correlation IDs via a Monolog processor, not call sites. The cleanest way to get request/job IDs on every record is a processor registered once:

// app/Providers/AppServiceProvider.php
use Monolog\LogRecord;
use Monolog\Processor\ProcessorInterface;

Log::extend('betterstack', fn ($app, $config) => /* decorate with processor */);

or, more simply, push into the shared context in a middleware:

Log::shareContext([
    'request_id' => (string) Str::uuid(),
    'release'    => config('app.release'),
]);

Log::shareContext() is usually enough and avoids touching the driver at all. Add a job/attempt pair inside queue jobs the same way.

Per-domain channels for signal separation. High-volume subsystems deserve their own channel with its own level or source token:

'betterstack-audit' => [
    'driver' => 'betterstack',
    'token'  => env('BETTERSTACK_AUDIT_TOKEN'),
    'level'  => 'info',
],

Then Log::channel('betterstack-audit')->info(...). This keeps a noisy integration from drowning the errors you actually page on, and lets the audit stream keep a lower retention/alerting policy.

Exceptions: log the context you'll want at 3am. In bootstrap/app.php:

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->report(function (Throwable $e) {
        Log::error($e->getMessage(), [
            'exception' => $e,
            'route'     => request()->route()?->getName(),
            'user_id'   => auth()->id(),
            'request_id'=> /* from shared context */,
        ]);
    });
})

Pair with $exceptions->dontReport() for the noise you never read (ValidationException, 404s) so quota goes to things that matter.

Queue and scheduler visibility. Failed jobs are why most teams adopt a hosted log service in the first place. Log start/finish/failure with job, attempt, duration_ms, and the queue name. Queue::failing() in a service provider gives you one hook for the whole app.

Sampling for chatty paths. Don't lower the channel level to cut volume — you'll lose the warnings too. Instead:

if (app()->environment('production') && random_int(1, 100) > 5) {
    return;
}

Guard verbose logging behind a config flag or a sample rate, and keep the channel at warning.

Testing. Assert on the log contract, not the driver:

Log::spy();
$this->post('/orders', $payload);
Log::shouldHaveReceived('warning')->withArgs(
    fn ($message, $context) => $context['order_id'] === $order->id
);

Keep LOG_CHANNEL=single (or a null channel) in phpunit.xml so the suite never makes network calls. If you need the real channel shape in a test, Log::build([...]) constructs one without config changes.


Gotchas and Tips

A bad token is indistinguishable from no traffic. Logging handlers swallow transport errors by design — a logging failure must never take down the app. The consequence is that a rotated token, a typo, or an expired source can go unnoticed for weeks. Mitigations: fire a deliberate smoke-test log in your deploy pipeline, alert on "no records received in N minutes" in Better Stack itself, and re-verify after any deploy that touches config/logging.php or .env. v1.4.0 improved error handling here, but "improved" does not mean "loud" — verify rather than assume.

Config caching will bite you. env() outside config files returns null once config:cache has run. Keep the token read in config/logging.php only, and remember that changing .env on a cached environment requires php artisan config:cache again. This is the #1 reason a newly added channel "does nothing."

Synchronous HTTP in the request path. Hosted drivers make a network call. If the handler is synchronous (read the provider to confirm), a slow Better Stack endpoint adds tail latency to every request that logs — and in the worst case to exception rendering, which is exactly when you can least afford it. Watch p99 under load; if it moves, favour buffering, or a queued path, and keep the local file sink regardless.

PII leaves your infrastructure. dontFlash/dontReport cover framework-level exception serialization but not your own Log::info() calls. Tokens, passwords, authorization headers, full request bodies, and personal data all ship to a third party. Add a redaction helper (Log::info('...', $this->redact($payload))) or a Monolog processor that strips known-sensitive keys, and make it a review checklist item. Laravel's $dontFlash and a strict context convention are cheap; a GDPR conversation is not.

Level filtering happens before ingestion. Setting the channel to warning means debug records never leave the process — you cannot "just go back" and find them later. Decide verbosity deliberately per environment, and keep a dedicated debug channel you can raise temporarily rather than lowering the global one.

Ingestion lag and ordering. Hosted log pipelines buffer; expect a few seconds before a record is queryable, and don't rely on strict ordering across records. This is fine for debugging, wrong for anything with correctness requirements.

Duplicate-looking entries. If you stack both daily and betterstack, the same line appears in both places. That's intended — but when auditing "why did this log twice," check whether a channel got composed into two stacks.

Queue workers and Octane. Long-lived workers don't get a fresh container per request. If you set request context (like user_id) in a middleware, clear it after each job/request or it will leak across unrelated records.

Monolog and Laravel major upgrades. Logging drivers are tightly coupled to Monolog's handler API and LogManager. Check the package's composer.json constraints before bumping Laravel or Monolog majors, and run a smoke test after. Logging packages are a classic "works on 10, breaks on 11" surface.

Quota creep. Ingestion is metered. A debug channel on a busy app burns quota fast, and the resulting throttling looks exactly like dropped logs. Set levels per environment, sample hot paths, and check the Better Stack usage dashboard after the first week rather than after the first invoice.

Bus factor. Single-maintainer package with modest adoption. The integration surface is tiny (one channel), so the exit cost is low — but vendor the config in your own repo, keep the stack composition so removal is a one-line change, and note that any custom formatter/processor you attach should live in your code, not in a fork.

Debugging checklist when logs don't appear:

  1. php artisan about — confirm the resolved default channel.
  2. php artisan config:clear (or config:cache) and retry.
  3. Confirm the token is in the runtime environment, not just your local .env.
  4. Check storage/logs/laravel.log — if the line is there but not in Better Stack, the problem is the driver/token/network, not your Log:: call.
  5. Temporarily drop the channel level to debug to rule out filtering.
  6. Fire a request with an intentionally invalid token in staging and confirm you can see the failure. That validation step is worth more than any amount of config review.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels