Building a Multilingual Activity Logging System in Laravel

Activity logging is essential for tracking user actions in web applications. This article will guide you through implementing a comprehensive activity logging system in Laravel with multilingual support, from database setup to displaying logs.


This content originally appeared on DEV Community and was authored by Hoang Manh Cam

Activity logging is essential for tracking user actions in web applications. This article will guide you through implementing a comprehensive activity logging system in Laravel with multilingual support, from database setup to displaying logs.

Step 1: Create the Database Structure

First, create a migration for the activity logs table:

php artisan make:migration create_activity_logs_table

In the migration file, define the structure:

public function up(): void
{
    Schema::create('activity_logs', function (Blueprint $table) {
        $table->bigInteger('id', true)->unsigned()->comment('ID');
        $table->string('record_table')->comment('Target Record Model');
        $table->bigInteger('record_id')->unsigned()->comment('Target Record ID');
        $table->string('action')->comment('Method');
        $table->string('ip_address')->comment('IP Address');
        $table->string('user_agent')->comment('User Agent');
        $table->string('user_table')->comment('User Model');
        $table->bigInteger('user_id')->unsigned()->nullable()->comment('User ID');
        $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP'));
        $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP'));
        $table->timestamp('deleted_at')->nullable();

        // Add indexes for better performance
        $table->index(['user_table', 'user_id']);
        $table->index(['record_table', 'record_id']);
        $table->index('action');
    });
}

Run the migration:

php artisan migrate

Step 2: Create the ActivityLog Model

Create a model that handles activity logs:

php artisan make:model ActivityLog

Implement the model with dynamic description generation:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class ActivityLog extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'record_table',
        'record_id',
        'action',
        'ip_address',
        'user_agent',
        'user_table',
        'user_id',
    ];

    protected $appends = ['description'];

    public function getDescriptionAttribute(): string
    {
        // The user who performed the action
        $user = $this->getRecord($this->user_table, $this->user_id);

        // The record on which the action was performed
        $record = $this->getRecord($this->record_table, $this->record_id);

        if (!$user || !$record) {
            return '';
        }

        $action = $this->action;
        $user_table = __("label.tables.$this->user_table");
        $record_table = __("label.tables.$this->record_table");

        // Format the description based on the action type
        return match ($action) {
            default => __(
                "activity_log.descriptions.$action",
                [
                    'user_table' => $user_table, 
                    'user_name' => $user->activity_log_name,
                    'record_table' => $record_table, 
                    'record_name' => $record->activity_log_name,
                    'record_id' => $this->record_id,
                ]
            ),
            'export' => __(
                "activity_log.descriptions.export",
                ['user_table' => $user_table, 'user_name' => $user->activity_log_name, 'record_table' => $record_table]
            ),
            'import' => __(
                "activity_log.descriptions.import",
                ['user_table' => $user_table, 'user_name' => $user->activity_log_name, 'record_table' => $record_table]
            ),
            // Other custom actions
        };
    }

    protected function getRecord($table, $id)
    {
        // Get model name from config
        $model = config("const.models.$table");
        if (class_exists($model)) {
            $query = $model::query();

            // Handle deleted records gracefully
            return $query->find($id) ?? new class {
                public string $activity_log_name;

                public function __construct()
                {
                    $this->activity_log_name = __('label.labels.deleted');
                }
            };
        }

        return null;
    }
}

Step 3: Create the ActivityLogService

Create a service to handle activity log creation:

<?php

namespace App\Services;

use App\Helpers\UserHelper;
use App\Models\ActivityLog;

class ActivityLogService
{
    public function save(string $recordTable, string $recordId, string $action): ActivityLog
    {
        $activityLog = new ActivityLog();
        $activityLog->record_table = $recordTable;
        $activityLog->record_id = $recordId;
        $activityLog->action = $action;
        $activityLog->ip_address = request()->ip() ?? 'undefined';
        $activityLog->user_agent = request()->userAgent() ?? 'undefined';
        $activityLog->user_table = UserHelper::getAuthTable(auth()->user());
        $activityLog->user_id = auth()->id() ?? null;

        $activityLog->save();

        return $activityLog;
    }
}

Step 4: Create a Helper for Authentication Types

Create a helper to determine the authenticated user type:

<?php

namespace App\Helpers;

class UserHelper
{
    public static function getAuthTable($user = null): string
    {
        if (!$user) {
            return '';
        }

        return match (get_class($user)) {
            'App\Models\Admin' => 'admins',
            'App\Models\User' => 'users',
            'App\Models\Manager' => 'managers',
            default => '',
        };
    }
}

Step 5: Update Your Models

Add a method to each model that should be tracked in activity logs. For example User model:

public function getActivityLogNameAttribute(): string
{
    if ($this->trashed()) {
        return __('deleted');
    }

    $name = $this->full_name;
    if (Route::is('admin.*')) {
        return "<a href='" . route('admin.users.edit', $this->id) . "'>" . $name . "</a>";
    }
    return $name;
}

Step 6: Configure Model Mappings

In config/const.php, add model and icon mappings:

'models' => [
    'admins' => 'App\Models\Admin',
    'users' => 'App\Models\User',
    'settings' => 'App\Models\Setting',
    'activity_logs' => 'App\Models\ActivityLog',
    'managers' => 'App\Models\Manager',
],

'icons' => [
    'admins' => 'ph-user-gear',
    'users' => 'ph-users',
    'settings' => 'ph-gear',
    'languages' => 'ph-translate',
    'activity_logs' => 'ph-scroll',
    'notifications' => 'ph-bell',
    'managers' => 'ph-person-simple',
],

Step 7: Create Translation Files

Create language files for activity logs:

For English (lang/en/activity_log.php):

<?php

return [
    'actions' => [
        'store' => 'Create',
        'update' => 'Update',
        'destroy' => 'Delete',
        'import' => 'Import',
        'export' => 'Export',
        'update_password' => 'Update Password',
        // other actions
    ],
    'descriptions' => [
        'store' => ':user_table :user_name created :record_table :record_name.',
        'update' => ':user_table :user_name updated :record_table :record_name.',
        'destroy' => ':user_table :user_name deleted :record_table ID::record_id.',
        'import' => ':user_table :user_name imported :record_table.',
        'export' => ':user_table :user_name exported :record_table data.',
        // other descriptions
    ]
];

Please update similarly for other languages.

Step 8: Create the Controller

Create a controller to display activity logs:

php artisan make:controller Admin/ActivityLogController

Implement the controller:

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Services\DataTableService;
use App\Services\ActivityLogService;
use Illuminate\Http\Request;

class ActivityLogController extends Controller
{
    protected DataTableService $dataTableService;
    protected ActivityLogService $activityLogService;

    public function __construct(DataTableService $dataTableService, ActivityLogService $activityLogService)
    {
        $this->dataTableService = $dataTableService;
        $this->activityLogService = $activityLogService;
    }

    public function index(Request $request)
    {
        if ($request->ajax()) {
            $columns = [
                'id', 'record_table', 'record_id', 'action', 'ip_address', 
                'user_agent', 'user_table', 'user_id', 'created_at',
            ];
            $data = ActivityLog::select($columns);
            $response = $this->dataTableService->processDataTable($data, $request, $columns);

            return response()->json($response);
        }

        return view('admin.activity_logs.index');
    }
}

Step 9: Create the View

Create a view to display activity logs using DataTables:

@extends('admin.layouts.master')
@section('title')
    {{ __('label.tables.activity_logs') }}
@endsection

@section('center-scripts')
    <script src="{{URL::asset('assets/admin/js/vendor/tables/datatables/datatables.min.js')}}"></script>
@endsection

@section('content')
    <div class="content">
        <!-- Single row selection -->
        <div class="card">
            <table class="table datatable-selection-single" id="activity-log-table">
                <thead>
                <tr>
                    <th width="5%">{{ __('label.columns.common.id') }}</th>
                    <th width="15%">{{ __('label.columns.activity_logs.record') }}</th>
                    <th width="15%">{{ __('label.columns.activity_logs.action') }}</th>
                    <th width="25%">{{ __('label.columns.activity_logs.description') }}</th>
                    <th width="15%">{{ __('label.columns.activity_logs.ip_address') }}</th>
                    <th width="25%">{{ __('label.columns.activity_logs.user_agent') }}</th>
                </tr>
                </thead>
                <tbody>
                </tbody>
            </table>
        </div>
    </div>
@endsection

@section('scripts')
    <script>
      let dataTableLanguage = @json(trans('datatable'));
      let icons = @json(config('const.icons'));
      let models = @json(trans('label.tables'));
      let action = @json(trans('activity_log.actions'));
      $(document).ready(function() {
        const config = {
          tableId: 'activity-log-table',
          ajaxUrl: "{{ route('admin.activity_logs.index') }}",
          columns: [
            {'data': 'id'},
            {
              'data': 'record_table',
              'render': function(data, type, row) {
                return icons.hasOwnProperty(data) ? 
                  `<i class="${icons[data]}"></i> ${models[data]}` : data;
              },
            },
            {
              'data': 'action',
              'render': function(data, type, row) {
                return action.hasOwnProperty(data) ? action[data] : data;
              },
            },
            {'data': 'description'},
            {'data': 'ip_address'},
            {'data': 'user_agent'},
          ],
          language: dataTableLanguage,
        };

        initializeDataTable(config);
      });
    </script>
@endsection

For more details on implementing DataTables in Laravel, you can refer to our previous article: Implementing Dynamic DataTables in Laravel with Server-Side Processing.

Step 10: Register Routes

Add routes for activity logs:

Route::group(['middleware' => 'can:admin:activity_logs'], function () {
    Route::get('/activity_logs', [ActivityLogController::class, 'index'])
         ->name('activity_logs.index');
});

Step 11: Using the Activity Log Service

Here's how to use the service in controllers like UserController:

For creating records:

public function store(Request $request)
{
    // Create a new user
    $user = User::create($request->validated());

    // Log the activity
    $this->activityLogService->save(
        $user->getTable(),  // Table name
        (string)$user->id,  // Record ID
        __FUNCTION__        // Action (store)
    );

    return redirect()->route('admin.users.index')
        ->with('success', 'User created successfully.');
}

For updates:

public function update(User $user, UserRequest $request)
{
    $user->fill($request->all());
    $user->save();

    $this->activityLogService->save(
        $user->getTable(),
        (string)$user->id,
        __FUNCTION__
    );

    return redirect()->back()->with('success', 'User updated successfully.');
}

For custom actions like password updates:

public function updatePassword(User $user, UserPasswordRequest $request)
{
    $user->fill($request->all());
    $user->save();

    $this->activityLogService->save(
        $user->getTable(),
        (string)$user->id,
        "update_password"  // Custom action name
    );

    return redirect()->back()->with('success', 'Password updated successfully.');
}

How It Works

Saving Activity Logs:

  • Each controller action calls the ActivityLogService
  • The service records details like table name, record ID, action type
  • Context info (IP, user agent) is automatically captured

Dynamic Description Generation:

  • The getDescriptionAttribute() method formats descriptions based on action type
  • It uses translation strings with placeholders (:user_name, :record_name)
  • Models provide their own activity_log_name methods for custom formatting

Multilingual Support:

  • Descriptions and action names come from language files
  • The system automatically uses the current locale
  • Format strings differ between languages for natural phrasing

DataTable Integration:

  • Server-side processing handles large log volumes efficiently
  • Client-side rendering applies formatting to icons and action names
  • All strings come from translation files for consistency

Benefits of This Approach

  • Complete Audit Trail: Track who did what, when, and from where
  • Multilingual Support: Automatically adapts to the user's language
  • Contextual Formatting: Links to related records when appropriate
  • Graceful Handling: Works even when related records are deleted
  • Clean Architecture: Separation of concerns between models, service, and controller

This implementation provides a robust, multilingual activity logging system that can easily scale with your application's needs while maintaining excellent performance.


This content originally appeared on DEV Community and was authored by Hoang Manh Cam


Print Share Comment Cite Upload Translate Updates
APA

Hoang Manh Cam | Sciencx (2025-08-28T01:28:40+00:00) Building a Multilingual Activity Logging System in Laravel. Retrieved from https://www.scien.cx/2025/08/28/building-a-multilingual-activity-logging-system-in-laravel/

MLA
" » Building a Multilingual Activity Logging System in Laravel." Hoang Manh Cam | Sciencx - Thursday August 28, 2025, https://www.scien.cx/2025/08/28/building-a-multilingual-activity-logging-system-in-laravel/
HARVARD
Hoang Manh Cam | Sciencx Thursday August 28, 2025 » Building a Multilingual Activity Logging System in Laravel., viewed ,<https://www.scien.cx/2025/08/28/building-a-multilingual-activity-logging-system-in-laravel/>
VANCOUVER
Hoang Manh Cam | Sciencx - » Building a Multilingual Activity Logging System in Laravel. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/08/28/building-a-multilingual-activity-logging-system-in-laravel/
CHICAGO
" » Building a Multilingual Activity Logging System in Laravel." Hoang Manh Cam | Sciencx - Accessed . https://www.scien.cx/2025/08/28/building-a-multilingual-activity-logging-system-in-laravel/
IEEE
" » Building a Multilingual Activity Logging System in Laravel." Hoang Manh Cam | Sciencx [Online]. Available: https://www.scien.cx/2025/08/28/building-a-multilingual-activity-logging-system-in-laravel/. [Accessed: ]
rf:citation
» Building a Multilingual Activity Logging System in Laravel | Hoang Manh Cam | Sciencx | https://www.scien.cx/2025/08/28/building-a-multilingual-activity-logging-system-in-laravel/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.