Metrics for Laravel

repository·master·Indexed 43 days ago

https://github.com/directorytree/metrics

A package for recording and querying metrics such as page views, API calls, and signups in Laravel applications. It features a Redis driver for high-traffic scenarios, support for hourly granularity, Eloquent model association via the HasMetrics trait, and a fake implementation for testing.

Tokens
8.7K
Snippets
33
Records
35
Agent score
54%

What's inside Metrics

  1. How hourly metrics work

    master

    By default, metrics are recorded at a daily level. To track metrics with hour-level granularity, use the hourly() method.

    Important: Hourly metrics are stored separately from daily metrics even if they share the same name. Using hourly metrics increases database row count by approximately 24x. Use them only when hour-level granularity or timezone-specific querying is required.

    // Record an hourly metric
    metric('api:requests')->hourly()->record();
    
    // Querying hourly metrics
    use DirectoryTree\\Metrics\\Metric;
    
    // Get metrics for this hour
    $metrics = Metric::thisHour()->get();
    
    // Get metrics for last hour
    $metrics = Metric::lastHour()->get();
    
    // Get metrics for a specific date and hour
    $metrics = Metric::onDateTime(Carbon::parse('2025-10-19 14:00:00'))->get();
  2. Organize metrics with categories

    master

    Use the category() method to segment metrics. Metrics with the same name but different categories are stored as separate entries, allowing you to track the same metric type across different contexts (e.g., different endpoints or error severities).

    // Track API calls by endpoint
    metric('api:requests')->category('users')->record();
    metric('api:requests')->category('orders')->record();
    
    // Track errors by severity
    metric('app:errors')->category('critical')->record();
  3. Capture and commit metrics in batches

    master

    For high-performance scenarios, use Metrics::capture() to buffer metrics in memory instead of writing to the database immediately. You can then use Metrics::commit() to write them all at once. Metrics are automatically committed when the application terminates. To enable this globally, call Metrics::capture() in your AppServiceProvider::boot() method.

    use DirectoryTree\\Metrics\\Facades\\Metrics;
    
    // Start capturing in memory
    Metrics::capture();
    
    metric('signups')->record();
    metric('api:requests')->category('web')->record();
    
    // Commit all at once
    Metrics::commit();
  4. Record basic metrics

    master

    You can record metrics using three different interfaces. All methods result in the same recording behavior. By default, a metric has a value of 1.

    // 1. Using the Metrics facade
    use DirectoryTree\
    Metrics\\MetricData;
    use DirectoryTree\\Metrics\\Facades\\Metrics;
    Metrics::record(new MetricData('signups'));
    
    // 2. Using the metric() global helper (recommended for brevity)
    metric('signups')->record();
    
    // 3. Using the PendingMetric class
    use DirectoryTree\\Metrics\\PendingMetric;
    PendingMetric::make('signups')->record();
  5. Record metrics with custom attributes

    master

    You can add custom context to metrics using the with() method. This is useful for segmenting metrics by dimensions like source, country, or device.

    Note:

    1. You must first add these columns to your metrics table via a migration.
    2. Custom attributes are included in the uniqueness check; metrics with different attribute values are stored as separate rows.
    3. Core attributes (name, category, year, month, day, measurable_type, measurable_id, value) cannot be overridden via with().
    // 1. Migration setup
    Schema::table('metrics', function (Blueprint $table) {
        $table->string('source')->nullable()->index();
        $table->string('country')->nullable()->index();
    });
    
    // 2. Recording with attributes
    metric('views')
        ->with(['source' => 'google', 'country' => 'US'])
        ->record();
    
    // 3. Querying by attributes
    $googleViews = Metric::where('name', 'views')
        ->where('source', 'google')
        ->sum('value');
  6. Associate metrics with Eloquent models

    master

    To track metrics per specific model instance (e.g., logins per user), use the HasMetrics trait in your Eloquent model. You can then use the measurable() method to associate the metric with that instance.

    // 1. Add trait to model
    use DirectoryTree\\Metrics\\HasMetrics;
    use Illuminate
    Database\\Eloquent\\Model;
    
    class User extends Model
    {
        use HasMetrics;
    }
    
    // 2. Record metric for a specific instance
    metric('auth:logins')->measurable($user)->record();
    
    // 3. Query metrics for a model
    $totalLogins = $user->metrics()->where('name', 'auth:logins')->sum('value');
  7. Use the Redis driver for high-traffic scenarios

    master

    For distributed applications or high-traffic scenarios, use the redis driver. This stores captured metrics in Redis before committing them to the database in batches, reducing database load.

    1. Set the driver to redis in config/metrics.php or via the METRICS_DRIVER environment variable.
    2. Important: If you plan to use the manual commit command, you must disable auto_commit in your configuration.
    3. Schedule the metrics:commit command to periodically move metrics from Redis to your database.
    // config/metrics.php
    
    return [
        'driver' => 'redis',
        // ...
    ];
    METRICS_DRIVER=redis
    // app/Console/Kernel.php
    
    protected function schedule(Schedule $schedule): void
    {
        $schedule->command('metrics:commit')->hourly();
    }
  8. Install the Metrics package

    master

    Install the package via Composer, publish the migrations, and run them to set up the necessary database tables.

    composer require directorytree/metrics
    
    php artisan vendor:publish --tag="metrics-migrations"
    
    php artisan migrate
  9. Test metrics using the fake implementation

    master

    For testing purposes, the package provides a fake implementation of the metrics system. You can use Metrics::fake() to prevent actual recording and instead use assertion methods to verify that metrics were recorded correctly during your tests.

    Available assertions:

    • Metrics::assertRecorded('name'): Asserts a metric with the given name was recorded.
    • Metrics::assertRecorded(fn (Measurable $metric) => ...): Asserts a metric was recorded using a closure for detailed validation (e.g., checking name, category, or measurable).
    • Metrics::assertNotRecorded('name'): Asserts a metric was NOT recorded.
    • Metrics::assertRecordedTimes('name', count): Asserts a metric was recorded a specific number of times.

    You can also retrieve recorded metrics in your tests using:

    • Metrics::recorded(): Returns all recorded metrics.
    • Metrics::recorded('name'): Returns metrics filtered by name.
    • Metrics::recorded(fn ($metric) => ...): Returns metrics filtered by a custom closure.
    use DirectoryTree\Metrics\Measurable;
    use DirectoryTree\Metrics\Facades\Metrics;
    
    public function test_user_signup_records_metric()
    {
        Metrics::fake();
    
        // Your code that records metrics...
        $this->post('register', [
            'email' => 'user@example.com',
            'password' => 'password',
        ]);
    
        // Assert metrics were recorded
        Metrics::assertRecorded('signups');
    }
    
    public function test_batch_job_records_metrics()
    {
        Metrics::fake();
    
        // Run your batch job
        Artisan::call('orders:process');
    
        // Assert metrics were recorded a specific number of times
        Metrics::assertRecordedTimes('orders:processed', 100);
    }
  10. Record metrics with custom dates

    master

    Metrics are recorded with today's date by default. Use the date() method to backfill data or record metrics for a specific past date.

    use Carbon\\Carbon;
    
    // Backfill signup data
    metric('signups')
        ->date(Carbon::parse('2025-01-15'))
        ->record(50);
  11. Use a global custom metric model

    master

    If you want to use a custom Eloquent model for all metrics instead of the default DirectoryTree\Metrics\Metric model, you must ensure your model:

    1. Is fully unguarded (protected $guarded = [];).
    2. Includes the same columns as the default metrics table.
    3. Includes the same casts as the default Metric model:
      • year => integer
      • month => integer
      • day => integer
      • hour => integer
      • value => integer

    Register the custom model globally in your AppServiceProvider using DatabaseMetricManager::useModel():

    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Model;
    
    class CustomMetric extends Model
    {
        protected $guarded = [];
    
        protected function casts(): array
        {
            return [
                'year' => 'integer',
                'month' => 'integer',
                'day' => 'integer',
                'hour' => 'integer',
                'value' => 'integer',
            ];
        }
    }
    
    // In AppServiceProvider.php
    use App\Models\CustomMetric;
    use DirectoryTree\Metrics\DatabaseMetricManager;
    
    public function boot(): void
    {
        DatabaseMetricManager::useModel(CustomMetric::class);
    }