kbeaud11/laravel-cloud-tracker

Track usage-based infrastructure costs per model in Laravel Cloud environments.

Maintainers

Package info

github.com/Kbeaud11/laravel-cloud-tracker

pkg:composer/kbeaud11/laravel-cloud-tracker

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1 2026-03-25 18:45 UTC

This package is auto-updated.

Last update: 2026-03-25 18:48:04 UTC


README

Track usage-based infrastructure costs per model in Laravel Cloud environments. Built for observability — not billing.

Laravel Cloud Tracker gives you per-entity, per-feature cost visibility by wrapping expensive operations in a fluent API that measures execution time, calculates estimated cost across multiple infrastructure dimensions, and stores both granular events and aggregated monthly rollups.

Why?

Laravel Cloud bills by usage: compute time, database CPU, cache operations, bandwidth, and more. When you run a multi-tenant SaaS, you need to understand which tenants consume which resources and how much it costs.

This package answers questions like:

  • "How much compute does Organization X consume monthly?"
  • "What's the most expensive feature across all tenants?"
  • "Are our enterprise clients actually covering their infrastructure costs?"

It does not enforce billing, integrate with Stripe, or render UI. It's the observability layer that makes those things possible.

Requirements

  • PHP 8.2+
  • Laravel 11 or 12
  • MySQL, PostgreSQL, or SQLite

Installation

composer require kbeaud11/laravel-cloud-tracker

Publish Configuration

php artisan vendor:publish --tag=cloud-tracker-config

Publish & Run Migrations

php artisan vendor:publish --tag=cloud-tracker-migrations
php artisan migrate

This creates three tables:

Table Purpose
model_tracking_policies Per-model tracking configuration (mode, feature lists, multiplier)
model_usage_events Granular event log for every tracked operation
model_usage_rollups Monthly aggregates per model + feature for fast querying

Configuration

After publishing, the config lives at config/cloud-tracker.php.

Master Switch & Environments

'enabled' => env('CLOUD_TRACKER_ENABLED', true),

'environments' => ['production', 'staging'],

Tracking is disabled by default in local development. The callback always executes — only timing, cost calculation, and DB writes are skipped.

Laravel Cloud Plan

'plan' => env('CLOUD_TRACKER_PLAN', 'growth'),

Stored for reference and future quota features. Does not affect per-unit rates (those are determined by instance selection).

Supported values: starter, growth, business, enterprise.

Cost Dimensions

Every billable Laravel Cloud resource is represented as a cost dimension. Rates are pre-populated from Laravel Cloud's published pricing (US East region).

Compute (Time-Based)

Select your instance size via environment variable:

CLOUD_TRACKER_COMPUTE_INSTANCE=pro-2c-4g

Available instances:

Instance Monthly Per Second
flex-1c-256m $4/mo $0.00000165/s
flex-2c-512m $8/mo $0.00000331/s
flex-4c-2g $16/mo $0.00000661/s
pro-1c-1g $20/mo $0.00000827/s
pro-2c-4g $40/mo $0.00001650/s
pro-4c-8g $80/mo $0.00003310/s

Queue Workers (Time-Based)

Queue workers use the same instance pricing as compute but are configured independently:

CLOUD_TRACKER_QUEUE_INSTANCE=flex-2c-512m

Serverless Postgres (Time-Based)

Billed at $0.106/hour per vCPU:

'postgres' => [
    'unit' => 'time',
    'per_second' => 0.00002944,
],

Cache / Valkey (Flat Monthly)

Flat monthly rate amortized over estimated operations:

CLOUD_TRACKER_CACHE_TIER=1g
Tier Monthly
250m $6/mo
1g $20/mo
2g $40/mo
5g $80/mo
10g $140/mo
25g $200/mo
50g $272/mo

Adjust estimated_operations_per_month to match your actual usage for more accurate per-operation cost.

WebSockets / Reverb (Flat Monthly)

CLOUD_TRACKER_WEBSOCKET_MONTHLY=5.00

Amortized over estimated_messages_per_month (default: 1,000,000).

Bandwidth (Count-Based)

'bandwidth' => [
    'unit' => 'count',
    'per_gb' => 0.10,
],

Object Storage (Count-Based)

'storage' => [
    'unit' => 'count',
    'per_1k_operations' => 0.0005,
    'per_gb_month' => 0.02,
],

Event Logging

'log_events' => true,

Set to false to skip writing to model_usage_events and only maintain rollups. Reduces write volume in high-throughput scenarios.

Default Dimension

'default_dimension' => 'compute',

Applied when no dimension is explicitly chained on a track() call.

Usage

1. Add the Trait to Your Billable Model

use LaravelCloudTracker\Traits\TracksCloudCost;

class Organization extends Model
{
    use TracksCloudCost;
}

This provides three relationships:

$org->trackingPolicy;  // MorphOne — the model's tracking configuration
$org->usageEvents;     // MorphMany — all granular usage events
$org->usageRollups;    // MorphMany — monthly rollup aggregates

2. Track an Operation

use LaravelCloudTracker\Facades\CloudCost;

$result = CloudCost::for($organization)
    ->feature('smart_segment_rebuild')
    ->track(function () use ($segment) {
        return $segment->rebuild();
    });

The callback's return value is always passed through. Timing, cost calculation, event logging, and rollup upsert happen transparently.

3. Chain Multiple Dimensions

When an operation spans multiple infrastructure resources:

CloudCost::for($organization)
    ->feature('live_dashboard_update')
    ->dimension('compute')
    ->dimension('postgres')
    ->dimension('cache', operations: 25)
    ->dimension('websocket', quantity: 3)
    ->track(function () use ($dashboard) {
        $data = $dashboard->aggregateMetrics();    // compute + postgres
        Cache::put("dashboard:{$dashboard->id}", $data); // cache
        broadcast(new DashboardUpdated($data));    // websocket
        return $data;
    });

Each dimension calculates cost independently based on its unit type, then all are summed.

For time-based dimensions (compute, postgres, queue), cost is derived from execution time automatically.

For count-based and flat-monthly dimensions (cache, websocket, bandwidth, storage), pass a quantity:

->dimension('cache', quantity: 50)      // 50 cache operations
->dimension('websocket', quantity: 3)   // 3 messages broadcast
->dimension('bandwidth', quantity: 0.5) // 0.5 GB transferred

4. Force Tracking (Bypass Policy)

For admin or internal operations that should always be tracked regardless of the model's policy:

CloudCost::for($organization)
    ->feature('admin_data_export')
    ->force()
    ->track(fn () => $exporter->run());

force() bypasses the tracking policy but still respects environment restrictions (local stays disabled).

5. Attach Metadata

Store arbitrary context with the event for debugging or reporting:

CloudCost::for($organization)
    ->feature('csv_import')
    ->withMetadata([
        'job_id' => $this->job->getJobId(),
        'rows' => $rowCount,
        'file_size_mb' => $fileSizeMb,
    ])
    ->track(fn () => $importer->process($file));

Metadata is stored as JSON on the model_usage_events row.

Tracking Policies

Every billable model can have a tracking policy that controls which features are tracked and at what cost multiplier. Policies are stored in the model_tracking_policies table (created by the package migration).

Creating a Policy

use LaravelCloudTracker\Models\TrackingPolicy;
use LaravelCloudTracker\Enums\TrackingMode;

// Track everything (default behavior even without a policy row)
TrackingPolicy::create([
    'billable_type' => $org->getMorphClass(),
    'billable_id' => $org->getKey(),
    'tracking_mode' => TrackingMode::ALL,
]);

// Track nothing
TrackingPolicy::create([
    'billable_type' => $org->getMorphClass(),
    'billable_id' => $org->getKey(),
    'tracking_mode' => TrackingMode::NONE,
]);

// Only track specific features
TrackingPolicy::create([
    'billable_type' => $org->getMorphClass(),
    'billable_id' => $org->getKey(),
    'tracking_mode' => TrackingMode::ALLOWLIST,
    'tracking_features' => ['segment_rebuild', 'csv_import', 'merge'],
]);

// Track everything except listed features
TrackingPolicy::create([
    'billable_type' => $org->getMorphClass(),
    'billable_id' => $org->getKey(),
    'tracking_mode' => TrackingMode::DENYLIST,
    'tracking_features' => ['debug', 'health_check'],
]);

Or via the trait relationship:

$org->trackingPolicy()->create([
    'tracking_mode' => TrackingMode::ALL,
    'usage_multiplier' => 0.75, // Enterprise discount
]);

Tracking Modes

Mode Behavior
all Track every feature. This is also the default when no policy row exists.
none Track nothing. The callback still executes, but no timing or DB writes occur.
allowlist Only track features listed in tracking_features.
denylist Track everything except features listed in tracking_features.

Usage Multiplier

The usage_multiplier column scales all cost calculations for a model:

// Enterprise client at 75% rate
$org->trackingPolicy()->update(['usage_multiplier' => 0.7500]);

// At-cost client at 100% (default)
$org->trackingPolicy()->update(['usage_multiplier' => 1.0000]);

// Premium support client at 150% markup
$org->trackingPolicy()->update(['usage_multiplier' => 1.5000]);

Policy Resolution

Policy is evaluated before timing starts. When tracking is disabled for a model+feature:

  • The callback still executes normally
  • No hrtime calls
  • No cost calculation
  • No database writes
  • Near-zero overhead

Policies are cached in memory for the duration of the request to avoid redundant queries.

Querying Usage Data

Via Relationships

// All events for an organization
$org->usageEvents()->where('feature', 'merge')->get();

// Monthly rollups
$org->usageRollups()
    ->where('period_start', '2026-02-01')
    ->get();

// Total cost this month
$org->usageRollups()
    ->where('period_start', now()->startOfMonth())
    ->sum('total_cost');

Via Models Directly

use LaravelCloudTracker\Models\UsageRollup;
use LaravelCloudTracker\Models\UsageEvent;

// Top 10 most expensive features this month
UsageRollup::where('period_start', now()->startOfMonth())
    ->orderByDesc('total_cost')
    ->limit(10)
    ->get();

// Recent events with cost breakdown
UsageEvent::where('feature', 'segment_rebuild')
    ->latest()
    ->limit(50)
    ->get()
    ->each(function ($event) {
        // $event->cost_dimensions contains per-dimension breakdown:
        // [
        //     'compute' => ['ms' => 150.23, 'cost' => 0.00000024],
        //     'cache'   => ['quantity' => 25, 'cost' => 0.000015],
        // ]
    });

Extending the Package

Both the policy resolver and cost calculator are bound to contracts in the service container. You can swap in your own implementations.

Custom Policy Resolver

use LaravelCloudTracker\Contracts\TrackingPolicyResolver;

class CustomPolicyResolver implements TrackingPolicyResolver
{
    public function shouldTrack(Model $model, string $feature): bool
    {
        // Your custom logic — check feature flags, plan tiers, etc.
    }

    public function getMultiplier(Model $model): float
    {
        // Dynamic pricing based on plan, usage tier, time of day, etc.
    }

    public function flush(): void
    {
        // Clear any caches
    }
}

Register in your AppServiceProvider:

$this->app->singleton(
    \LaravelCloudTracker\Contracts\TrackingPolicyResolver::class,
    \App\Services\CustomPolicyResolver::class,
);

Custom Cost Calculator

use LaravelCloudTracker\Contracts\CostCalculator;

class VolumeTierCostCalculator implements CostCalculator
{
    public function calculate(float $executionTimeMs, array $dimensions, float $multiplier = 1.0): array
    {
        // Apply volume discounts, time-of-day pricing, etc.
    }
}

Register in your AppServiceProvider:

$this->app->singleton(
    \LaravelCloudTracker\Contracts\CostCalculator::class,
    \App\Services\VolumeTierCostCalculator::class,
);

How It Works

Execution Flow

CloudCost::for($model)->feature('x')->track(fn () => ...)
│
├─ Validate: model and feature are set
├─ Check: config enabled?
├─ Check: environment allowed?
├─ Check: policy allows tracking? (skipped if force())
│
├─ Start hrtime
├─ Execute callback
├─ Stop hrtime → execution_time_ms
│
├─ Resolve dimensions (default to config default_dimension)
├─ Calculate cost per dimension × usage_multiplier
│
├─ Write to model_usage_events (if log_events enabled)
├─ Atomic upsert to model_usage_rollups
│
└─ Return callback result

Cost Calculation

Each dimension type calculates cost differently:

Unit Type Formula Examples
time execution_time_ms ÷ 1000 × per_second_rate compute, postgres, queue
count quantity × per_unit_rate bandwidth, storage
flat_monthly quantity × (monthly_rate ÷ estimated_ops_per_month) cache, websocket

The total cost across all dimensions is then multiplied by the model's usage_multiplier.

Rollup Aggregation

Rollups use an atomic database upsert keyed by (billable_type, billable_id, feature, period_start). On conflict, total_execution_ms, total_cost, and event_count are incremented atomically — no read-then-write race conditions.

Compatible with MySQL, PostgreSQL, and SQLite.

Testing

Running Package Tests

composer install
./vendor/bin/phpunit

Tests run against an in-memory SQLite database with no external dependencies.

Test Coverage

Suite Tests Covers
Feature/TrackingPolicyTest 8 All policy modes, multiplier, caching, trait relationships
Feature/CloudCostTrackingTest 18 Full tracking lifecycle, force bypass, dimension chaining, environment/config disabling, timing accuracy, metadata, event logging toggle
Unit/CostCalculationTest 11 All dimension unit types, multiplier scaling, multi-dimension summation, edge cases, unknown dimension errors

Testing in Your Application

The package respects the environments config. Add 'testing' to track during tests, or leave it out to skip tracking entirely in your test suite:

// config/cloud-tracker.php
'environments' => ['production', 'staging', 'testing'],

To assert tracking in your application tests:

use LaravelCloudTracker\Models\UsageEvent;
use LaravelCloudTracker\Models\UsageRollup;

// Assert an event was recorded
$this->assertDatabaseHas('model_usage_events', [
    'billable_type' => $org->getMorphClass(),
    'billable_id' => $org->getKey(),
    'feature' => 'merge',
]);

// Assert rollup was created
$this->assertDatabaseHas('model_usage_rollups', [
    'feature' => 'merge',
    'period_start' => now()->startOfMonth()->toDateString(),
]);

What This Package Is Not

  • Not a billing engine. No Stripe, no invoices, no payment processing.
  • Not a UI. No dashboards, charts, or admin panels.
  • Not real infrastructure introspection. It doesn't read CloudWatch metrics or parse SQL queries. It estimates cost from execution time and configured rates.
  • Not a rate limiter or quota enforcer. It observes — it doesn't restrict.

It is the observability foundation that makes all of those things possible.

License

MIT