├── .env.testing ├── .github └── workflows │ └── unittest.yml ├── .gitignore ├── LICENSE.md ├── codecov.yml ├── composer.json ├── docker-compose.yaml ├── docker └── php8 │ └── Dockerfile ├── phpunit.xml ├── readme.md ├── runLocalTestInDocker.sh ├── src ├── Commands │ └── LogCleanerUpper.php ├── Facades │ └── LogToDB.php ├── Jobs │ └── SaveNewLogEvent.php ├── LogToDB.php ├── LogToDbCustomLoggingHandler.php ├── LogToDbHandler.php ├── Models │ ├── BindsDynamically.php │ ├── DBLog.php │ ├── DBLogException.php │ ├── DBLogMongoDB.php │ └── LogToDbCreateObject.php ├── Processors │ ├── AuthenticatedUserProcessor.php │ └── PhpVersionProcessor.php ├── ServiceProvider.php ├── config │ └── logtodb.php └── migrations │ └── 2018_08_11_003343_create_log_table.php ├── testbench.yaml └── tests ├── FailureTest.php ├── LogToDbTest.php └── TestModels ├── CustomEloquentModel.php ├── LogMongo.php └── LogSql.php /.env.testing: -------------------------------------------------------------------------------- 1 | DB_CONNECTION=mysql 2 | DB_HOST=127.0.0.1 3 | DB_PORT=3306 4 | DB_DATABASE=logtodb 5 | DB_USERNAME=root 6 | DB_PASSWORD=root 7 | MDB_DATABASE=logtodb 8 | MDB_HOST=127.0.0.1 9 | MDB_PORT=27017 10 | -------------------------------------------------------------------------------- /.github/workflows/unittest.yml: -------------------------------------------------------------------------------- 1 | # GitHub Action for Laravel with MySQL and Redis 2 | name: Testing Laravel with MySQL 3 | on: [pull_request] 4 | jobs: 5 | laravel: 6 | name: Laravel ${{ matrix.laravel }} (PHP ${{ matrix.php }}) 7 | runs-on: ubuntu-latest 8 | env: 9 | DB_DATABASE: logtodb 10 | DB_USERNAME: root 11 | DB_PASSWORD: root 12 | BROADCAST_DRIVER: log 13 | CACHE_DRIVER: file 14 | QUEUE_CONNECTION: file 15 | SESSION_DRIVER: file 16 | 17 | # Docs: https://docs.github.com/en/actions/using-containerized-services 18 | services: 19 | mysql: 20 | image: mysql:latest 21 | env: 22 | MYSQL_ALLOW_EMPTY_PASSWORD: false 23 | MYSQL_ROOT_PASSWORD: root 24 | MYSQL_DATABASE: logtodb 25 | ports: 26 | - 3306/tcp 27 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | laravel: [^11.0, ^12.0] 33 | php: [8.3, 8.4] 34 | include: 35 | - laravel: ^11.0 36 | testbench: ^9.0 37 | - laravel: ^12.0 38 | testbench: ^10.0 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | 43 | - name: Start MongoDB 44 | uses: supercharge/mongodb-github-action@1.10.0 45 | with: 46 | mongodb-version: 7.0 47 | 48 | # Docs: https://github.com/shivammathur/setup-php 49 | - name: Setup PHP 50 | uses: shivammathur/setup-php@v2 51 | with: 52 | php-version: ${{ matrix.php }} 53 | extensions: mbstring, dom, fileinfo, mysql, mongodb 54 | coverage: xdebug 55 | 56 | # Local MySQL service in GitHub hosted environments is disabled by default. 57 | # If you are using it instead of service containers, make sure you start it. 58 | # - name: Start mysql service 59 | # run: sudo systemctl start mysql.service 60 | 61 | - name: Install Composer dependencies 62 | run: | 63 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 64 | composer update --prefer-dist --no-interaction --no-progress 65 | 66 | - name: Test with phpunit 67 | run: vendor/bin/phpunit --coverage-clover ./coverage.xml 68 | env: 69 | DB_PORT: ${{ job.services.mysql.ports['3306'] }} 70 | 71 | - name: Upload to Codecov 72 | uses: codecov/codecov-action@v2 73 | with: 74 | token: ${{ secrets.CODE_COV_TOKEN }} 75 | files: ./coverage.xml 76 | verbose: true 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .AppleDouble 3 | .LSOverride 4 | /node_modules 5 | /public/storage 6 | /vendor 7 | /.idea 8 | /bin 9 | Homestead.json 10 | Homestead.yaml 11 | .env 12 | .vagrant 13 | composer.lock 14 | coverage.xml 15 | .phpstorm.meta.php 16 | _ide_helper.php 17 | _ide_helper_models.php.php 18 | .phpunit.result.cache 19 | .phpunit.cache/* -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Daniel Mellum 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | branch: main 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "danielme85/laravel-log-to-db", 3 | "description": "Custom Laravel Log channel handler that can store log events to SQL or MongoDB databases. Uses Laravel native logging functionality.", 4 | "keywords": [ 5 | "laravel", 6 | "logging", 7 | "log", 8 | "database", 9 | "db", 10 | "mysql", 11 | "sql", 12 | "mongodb" 13 | ], 14 | "type": "library", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Daniel Mellum", 19 | "email": "mellum@gmail.com" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=8.1", 24 | "illuminate/support": ">=10.0" 25 | }, 26 | "require-dev": { 27 | "ext-mongodb": "*", 28 | "phpunit/phpunit": ">=10.0", 29 | "orchestra/testbench": ">=8.0", 30 | "mockery/mockery": ">=1.5", 31 | "nunomaduro/collision": ">=7.0", 32 | "mongodb/laravel-mongodb": ">=4" 33 | }, 34 | "suggest": { 35 | "jenssegers/mongodb": "Adds support for MongoDB in Laravel/Eloquent" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "danielme85\\LaravelLogToDB\\": "src" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "TestModels\\": "tests/TestModels" 45 | } 46 | }, 47 | "extra": { 48 | "laravel": { 49 | "providers": [ 50 | "danielme85\\LaravelLogToDB\\ServiceProvider" 51 | ], 52 | "aliases": { 53 | "LogToDB": "danielme85\\LaravelLogToDB\\LogToDB" 54 | } 55 | } 56 | }, 57 | "minimum-stability": "dev" 58 | } -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | logto-mariadb: 4 | image: mariadb:latest 5 | container_name: logto-mariadb 6 | networks: 7 | - laravel-log-to-db-testing 8 | environment: 9 | MYSQL_DATABASE: 'logtodb' 10 | MYSQL_ROOT_PASSWORD: 'root' 11 | MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' 12 | 13 | logto-mongodb: 14 | image: mongo:latest 15 | container_name: logto-mongodb 16 | networks: 17 | - laravel-log-to-db-testing 18 | 19 | php8: 20 | build: 21 | context: docker/php8 22 | container_name: laravel-log-to-db-php8 23 | tty: true 24 | networks: 25 | - laravel-log-to-db-testing 26 | volumes: 27 | - .:/var/testing 28 | depends_on: 29 | - logto-mariadb 30 | - logto-mongodb 31 | environment: 32 | WAIT_HOSTS: logto-mariadb:3306, logto-mongodb:27017 33 | DB_CONNECTION: mysql 34 | DB_HOST: logto-mariadb 35 | DB_PORT: 3306 36 | DB_DATABASE: logtodb 37 | DB_USERNAME: root 38 | DB_PASSWORD: root 39 | MDB_DATABASE: logtodb 40 | MDB_HOST: logto-mongodb 41 | MDB_PORT: 27017 42 | entrypoint: bash -c " 43 | /wait && 44 | cd /var/testing && 45 | composer install --no-interaction && 46 | ./vendor/bin/phpunit 47 | " 48 | 49 | networks: 50 | laravel-log-to-db-testing: 51 | driver: bridge 52 | -------------------------------------------------------------------------------- /docker/php8/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:jammy 2 | 3 | LABEL Maintainer="Daniel Mellum " \ 4 | Description="A simple docker image used in phpunit testing Laravel apps." 5 | 6 | ENV DOCKERIZE_VERSION v0.7.0 7 | ENV TZ=America/New_York 8 | ARG DEBIAN_FRONTEND=noninteractive 9 | 10 | WORKDIR / 11 | 12 | RUN apt update && apt install -y lsb-release gnupg2 ca-certificates apt-transport-https software-properties-common 13 | RUN apt update && add-apt-repository ppa:ondrej/php 14 | RUN apt update && apt upgrade -y 15 | RUN apt install -y curl git openssl openssh-client mysql-client bash libzip-dev zip wget 16 | RUN apt install -y php8.3 php8.3-dev php8.3-mysql php8.3-mongodb php8.3-curl php8.3-mbstring php8.3-pcov php8.3-cli 17 | 18 | RUN pecl install pcov 19 | RUN pecl install mongodb 20 | 21 | #RUN mkdir -p /etc/php81/mods-available && echo "extension=mongodb.so" >> /etc/php81/mods-available/mongodb.ini 22 | 23 | RUN apt install wget -y 24 | 25 | RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ 26 | && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ 27 | && rm dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz 28 | 29 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 30 | 31 | ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.2.1/wait /wait 32 | RUN chmod +x /wait 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | ./tests/ 11 | ./tests/TestModels 12 | 13 | 14 | 15 | 16 | ./src 17 | 18 | 19 | ./vendor 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel Log-to-DB 2 | [![GitHub](https://img.shields.io/github/license/mashape/apistatus.svg?style=flat-square)](https://github.com/danielme85/laravel-log-to-db) 3 | [![PHP from Packagist](https://img.shields.io/packagist/php-v/danielme85/laravel-log-to-db.svg?style=flat-square)](https://packagist.org/packages/danielme85/laravel-log-to-db) 4 | [![GitHub release](https://img.shields.io/github/release/danielme85/laravel-log-to-db.svg?style=flat-square)](https://packagist.org/packages/danielme85/laravel-log-to-db) 5 | [![GitHub tag](https://img.shields.io/github/tag/danielme85/laravel-log-to-db.svg?style=flat-square)](https://github.com/danielme85/laravel-log-to-db) 6 | [![Codecov](https://img.shields.io/codecov/c/github/danielme85/laravel-log-to-db.svg?style=flat-square)](https://codecov.io/gh/danielme85/laravel-log-to-db) 7 | [![CodeFactor](https://img.shields.io/codefactor/grade/github/danielme85/laravel-log-to-db?style=flat-square)](https://www.codefactor.io/repository/github/danielme85/laravel-log-to-db) 8 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/danielme85/laravel-log-to-db/tree/main.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/danielme85/laravel-log-to-db/tree/main) 9 | 10 | Hi, this is a custom Laravel 5.6+ Log channel handler that can store log events to SQL or MongoDB databases. 11 | Uses Laravel native logging functionality trough [Monolog](https://github.com/Seldaek/monolog). 12 | 13 | * [Installation](#installation) 14 | * [Configuration](#configuration) 15 | * [Usage](#usage) 16 | * [Fetching Logs](#fetching-logs) 17 | * [Custom Eloquent Model](#custom-eloquent-model) 18 | * [Log Cleanup](#log-cleanup) 19 | * [Processors](#processors) 20 | * [Lumen Installation](#lumen-installation) 21 | * [Local Testing With Docker](#local-testing-with-docker) 22 | 23 | 24 | > :warning: For Laravel version 10 and beyond, please use version 4 or later of this package. 25 | >
For Laravel version 5.6 to 9, please use version 3.x.x. 26 | 27 | ## Installation 28 | Use the composer require or add to composer.json. 29 | ``` 30 | composer require danielme85/laravel-log-to-db 31 | ``` 32 | 33 | If you are using SQL database server to store log events you can use the migration included. The MongoDB driver does not require the migration. 34 | Copy the migration file for log the database table to your app (you can also do any changes you want or manually copy this file your database/migrations folder). 35 | ``` 36 | php artisan vendor:publish --tag=migrations --provider="danielme85\LaravelLogToDB\ServiceProvider" 37 | ``` 38 | Run the Laravel migration artisan command. 39 | ``` 40 | php artisan migrate 41 | ``` 42 | 43 | 44 | For optional MongoDB support you need to install jenseegers/mongodb addon to Laravel 45 | ``` 46 | #Optional mongodb support, not required! 47 | 48 | composer require jenssegers/mongodb 49 | ``` 50 | 51 | ## Configuration 52 | Starting with Laravel 5.6 and later, you will have a new config file: "config/logging.php". 53 | You will need to add an array under 'channels' for Log-to-DB here like so: 54 | ```php 55 | 'database' => [ 56 | 'driver' => 'custom', 57 | 'via' => danielme85\LaravelLogToDB\LogToDbHandler::class 58 | ... 59 | ], 60 | ``` 61 | These are the minimum required logging.php config settings to get started. Please note that the array index 'database' 62 | can be whatever string you like as long as it is unique to this logging config. 63 | You can also give the logging channel a name that later is referenced in a column in the DB table, this way you can have multiple 64 | logging-to-db channels. 65 | 66 | ```php 67 | 'channels' => [ 68 | 'stack' => [ 69 | 'name' => 'Log Stack', 70 | 'driver' => 'stack', 71 | 'channels' => ['database', 'other-database', 'file'], 72 | ], 73 | 'database' => [ 74 | 'driver' => 'custom', 75 | 'via' => danielme85\LaravelLogToDB\LogToDbHandler::class, 76 | 'name' => 'Basic DB Logging' 77 | ], 78 | 'other-database' => [ 79 | 'driver' => 'custom', 80 | 'via' => danielme85\LaravelLogToDB\LogToDbHandler::class, 81 | //'model' => App\Model\Log::class, //Your own optional custom model 82 | 'level' => env('APP_LOG_LEVEL', 'debug'), 83 | 'name' => 'My DB Log with a bunch more settings', 84 | 'connection' => 'default', 85 | 'collection' => 'log', 86 | 'detailed' => true, 87 | 'queue' => false, 88 | 'queue_name' => '', 89 | 'queue_connection' => '', 90 | 'max_records' => false, 91 | 'max_hours' => false, 92 | 'processors' => [ 93 | //Monolog\Processor\HostnameProcessor::class 94 | // .. 95 | ] 96 | ], 97 | ... 98 | ] 99 | ``` 100 | * driver = Required to trigger the log driver. 101 | * via = The Log handler class. 102 | * level = The minimum error level to trigger this Log Channel. 103 | * name = The channel name that will be stored with the Log event. Please note that if you use the stack driver the name value in the stack array is used. 104 | * connection = The DB connection from config/database.php to use (default: 'default'). 105 | * collection = The DB table or collection name. (Default: log). 106 | * detailed = Store detailed log on Exceptions like stack-trace (default: true). 107 | * processors = Array of additional processors. These will add additional info into the 'extra' field in the logged data. 108 | [More information about processors](#processors) 109 | 110 | More info about some of these options: https://laravel.com/docs/9.x/logging#customizing-monolog-for-channels 111 | 112 | There are some default settings and more information about configuring the logger in the 'logtodb.php' config file. 113 | This could be copied to your project if you would like edit it with the vendor publish command. 114 | ``` 115 | php artisan vendor:publish --tag=config --provider="danielme85\LaravelLogToDB\ServiceProvider" 116 | ``` 117 | You can also change these settings in your env file. 118 | ``` 119 | LOG_DB_CONNECTION='default' 120 | LOG_DB_DETAILED=false 121 | LOG_DB_MAX=100 122 | LOG_DB_QUEUE=false 123 | LOG_DB_QUEUE_NAME='logToDBQueue' 124 | LOG_DB_QUEUE_CONNECTION='default' 125 | LOG_DB_MAX_COUNT=false 126 | LOG_DB_MAX_HOURS=false 127 | LOG_DB_DATETIME_FORMAT='Y-m-d H:i:s:ms' 128 | ``` 129 | 130 | > **PLEASE NOTE**: Starting with v2.2.0, the datetime column will be saved as a string in the format given in 131 | > 'datetime_format' in logtodb.php config file, or the LOG_DB_DATETIME_FORMAT value in your .env file. 132 | 133 | #### Config priority order 134 | There are three places you can change different options when using log-to-db: 135 | 1. The config file: config/logtodb.php (after doing vendor:publish). 136 | 2. Your .env file will override settings in the logtodb.php config file. 137 | 3. The Laravel logging config file: config/logging.php. You need to add a custom array here as mentioned above, 138 | in this same array you can specify/override config settings specifically for that log channel. 139 | 140 | Config values set in point 1 & 2 would work as default for all new log channels you add in the "channels" array for the 141 | Laravel logging configuration (config/logging.php). 142 | 143 | #### Log Worker Queue 144 | It might be a good idea to save the log events with a Queue Worker. This way your server does not have to wait for 145 | the save process to finish. You would have to configure the Laravel Queue settings and run the Queue listener. 146 | https://laravel.com/docs/6.x/queues#running-the-queue-worker 147 | 148 | The queue can be enabled/disabled in any of the following places: 149 | * LOG_DB_QUEUE = true | in .env 150 | * queue_db_saves => true | in config/logtodb.php 151 | * queue => true | in the log channel config array -> config/logging.php 152 | 153 | ## Usage 154 | Since this is a custom log channel for Laravel, all "standard" ways of generating log events etc should work with 155 | the Laravel Log Facade. See https://laravel.com/docs/6.x/logging for more information. 156 | ```php 157 | Log::debug("This is an test DEBUG log event"); 158 | Log::info("This is an test INFO log event"); 159 | Log::notice("This is an test NOTICE log event"); 160 | Log::warning("This is an test WARNING log event"); 161 | Log::error("This is an test ERROR log event"); 162 | Log::critical("This is an test CRITICAL log event"); 163 | Log::alert("This is an test ALERT log event"); 164 | Log::emergency("This is an test EMERGENCY log event"); 165 | ``` 166 | You can also log to specific log channels: 167 | Log::channel('database')debug("This is an test DEBUG log event"); 168 | 169 | 170 | ## Fetching Logs 171 | The logging by this channel is done trough the Eloquent Model builder. 172 | LogToDB::model($channel, $connection, $collection); 173 | You can skip all function variables and the default settings from the config/logtodb.php will be used. 174 | ```php 175 | $model = LogToDB::model(); 176 | $model->get(); //All logs for default channel/connection 177 | ``` 178 | 179 | Some more examples of getting logs 180 | ```php 181 | $logs = LogToDB::model()->get(); 182 | $logs = LogToDB::model()->where('level_name', '=', 'INFO')->get(); 183 | ``` 184 | 185 | When getting logs for specific channel or DB connection and collection you can either use the channel name matching 186 | config/logging.php or connection name from config/databases.php. You can also specify collection/table name if needed as 187 | the third function variable when fetching the model. 188 | ```php 189 | $logsFromDefault = LogDB::model()->get(); //Get the logs from the default log channel and default connection. 190 | $logsFromChannel = LogDB::model('database')->get(); //Get logs from the 'database' log channel. 191 | $logsFromChannel = LogDB::model('customname')->get(); //Get logs from the 'customname' log channel. 192 | $logsFromMysql = LogToDB::model(null, 'mysql')->get(); //Get all logs from the mysql connection (from Laravel database config) 193 | $logsFromMongoDB = LogToDB::model(null, 'mongodb')->get(); //Get all logs from the mongodb connection (from Laravel database config) 194 | $logsFromMysqlTable = LogToDB::model(null, 'mysql', 'table')->get(); //Get all logs from the mysql table: 'table' 195 | ``` 196 | 197 | ## Custom Eloquent Model 198 | Since Laravel is supposed to use static defined collection/table names, 199 | it might be better to use your own model in your app for a more solid approach. 200 | You can use your own eloquent model by referencing it in the config, then adding the trait: "LogToDbCreateObject" 201 | 202 | ##### SQL 203 | ```php 204 | namespace App\Models; 205 | 206 | use danielme85\LaravelLogToDB\Models\LogToDbCreateObject; 207 | use Illuminate\Database\Eloquent\Model; 208 | 209 | class CustomLog extends Model 210 | { 211 | use LogToDbCreateObject; 212 | 213 | protected $table = 'log'; 214 | protected $connection = 'mysql'; 215 | 216 | } 217 | ``` 218 | 219 | ##### MongoDB 220 | ```php 221 | namespace App\Models; 222 | use danielme85\LaravelLogToDB\Models\LogToDbCreateObject; 223 | use Jenssegers\Mongodb\Eloquent\Model as Eloquent; 224 | 225 | class CustomLogMongo extends Eloquent 226 | { 227 | use LogToDbCreateObject; 228 | 229 | protected $collection = 'log'; 230 | protected $connection = 'mongodb'; 231 | 232 | } 233 | ``` 234 | 235 | LOG_DB_MODEL='App\Models\CustomLog' 236 | 237 | > **WARNING**: Fetching the model trough the dynamic Eloquent model (default behavior) have some side-effects as tables and connections are 238 | declared dynamically instead of assigned properties in the model class. Certain functions are broken like LogToDB::model->all(), while LogToDB::model->where()->get() will work as normal. 239 | > Using your own models avoids these problems. 240 | 241 | #### Model Closures and Observers 242 | You can either add [closures](https://laravel.com/docs/7.x/eloquent#events-using-closures) on your custom application model mentioned above, 243 | or add a [model observer](https://laravel.com/docs/7.x/eloquent#observers) for the default LogToDb models. 244 |
245 | Create a observer: 246 | ``` 247 | observe(LogObserver::class); 273 | } 274 | } 275 | ``` 276 | 277 | 278 | #### Adding tables/expanding collections 279 | The Log handler for SQL expects the following schema: 280 | ```php 281 | Schema::create('log', function (Blueprint $table) { 282 | $table->increments('id'); 283 | $table->text('message')->nullable(); 284 | $table->string('channel')->nullable(); 285 | $table->integer('level')->default(0); 286 | $table->string('level_name', 20); 287 | $table->integer('unix_time'); 288 | $table->text('datetime')->nullable(); 289 | $table->longText('context')->nullable(); 290 | $table->text('extra')->nullable(); 291 | $table->timestamps(); 292 | }); 293 | ``` 294 | This is the migration that ships with this plugin. You can add as many tables as you want, and reference them in the 'collection' config value. 295 | Collection = table, I used the term collection as it works for both SQL/noSQL. 296 | No migrations needed for MongoDB. 297 | 298 | No indexes are added per default, so if you fetch a lot of log results based on specific time ranges or types: it might be a good idea to add some indexes. 299 | 300 | ## Log Cleanup 301 | There are config values that you can set to specify the max number of log records to keep, or the max record age in hours. 302 | 303 | * logging.php channel array -> (max_records, max_hours). 304 | * .env file -> (LOG_DB_MAX_COUNT, LOG_DB_MAX_HOURS). 305 | 306 | These option is set to *false* per default, these have to be set to desired integers before you can run the "log:delete" artisan command. 307 | ``` 308 | php artisan log:delete 309 | ``` 310 | This command will delete records based on settings described above. Add this command to your Console/kernel.php, or run manually in cron etc to enable automatic cleanup. 311 | 312 | #### Manual Cleanup 313 | There is a helper function to remove the oldest log events and keep a specified number 314 | ```php 315 | LogToDB::removeOldestIfMoreThan(100); 316 | ``` 317 | Or based on date (most be valid date/datetime supported by strtotime()) 318 | http://php.net/manual/en/function.strtotime.php 319 | 320 | ```php 321 | LogToDB::model()->removeOlderThan('2019-01-01'); 322 | LogToDB::model()->removeOlderThan('2019-01-01 23:00:00'); 323 | ``` 324 | 325 | ## Processors 326 | Monolog ships with a set of [processors](https://github.com/Seldaek/monolog/tree/master/src/Monolog/Processor), these will generate additional data and populate the 'extra' field. 327 | I've also added a couple of example processors in this pacage under src/Processors. 328 | To enable processors you can add them to the log config array: 329 | ```php 330 | 'database' => [ 331 | 'driver' => 'custom', 332 | 'via' => danielme85\LaravelLogToDB\LogToDbHandler::class 333 | ... 334 | 'processors' => [ 335 | \danielme85\LaravelLogToDB\Processors\PhpVersionProcessor::class, 336 | Monolog\Processor\HostnameProcessor::class, 337 | ] 338 | 339 | ``` 340 | 341 | You could also create your own custom processor, make sure they implement [Monolog\Processor\ProcessorInterface](https://github.com/Seldaek/monolog/blob/master/src/Monolog/Processor/ProcessorInterface.php). 342 | 343 | ##### Example of custom processor 344 | ```php 345 | env('LOG_CHANNEL', 'stack'), 367 | 368 | 'channels' => [ 369 | 'stack' => [ 370 | 'driver' => 'stack', 371 | 'channels' => ['database', 'mongodb', 'single'], 372 | ], 373 | 374 | 'database' => [ 375 | 'driver' => 'custom', 376 | 'via' => danielme85\LaravelLogToDB\LogToDbHandler::class, 377 | 'level' => env('APP_LOG_LEVEL', 'debug'), 378 | 'connection' => 'default', 379 | 'collection' => 'log' 380 | 'detailed' => true, 381 | 'queue' => true 382 | 'queue_name' => 'logQueue' 383 | 'queue_connection' => 'redis', 384 | 'max_records' => 1000, 385 | 'max_hours' => 24, 386 | ], 387 | 388 | 'mongodb' => [ 389 | 'driver' => 'custom', 390 | 'via' => danielme85\LaravelLogToDB\LogToDbHandler::class, 391 | 'level' => 'debug', 392 | 'connection' => 'mongodb', 393 | 'collection' => 'log', 394 | 'detailed' => true, 395 | 'queue' => true 396 | 'queue_name' => 'logQueue' 397 | 'queue_connection' => 'redis' 398 | ], 399 | 400 | 'limited' => [ 401 | 'driver' => 'custom', 402 | 'via' => danielme85\LaravelLogToDB\LogToDbHandler::class, 403 | 'level' => 'warning', 404 | 'detailed' => false, 405 | 'max_rows' => 10, 406 | 'name' => 'limited', 407 | ] 408 | 409 | 'single' => [ 410 | 'driver' => 'single', 411 | 'path' => storage_path('logs/laravel.log'), 412 | 'level' => env('APP_LOG_LEVEL', 'debug'), 413 | ], 414 | //.... 415 | ] 416 | ``` 417 | 418 | ## Lumen Installation 419 | 420 | * Create a config folder in your projects root directory (if you don't already have one), then add a logging.php config file there. 421 | Use the [Laravel logging.php](https://github.com/laravel/laravel/blob/master/config/logging.php) config file as an example/starting point. 422 | You can also add all the other "missing" config files from the Laravel config folder to your Lumen project. 423 | 424 | * To use these config files you have to load them in your applications bootstrap/app.php file (or add your own service provider file and load it in that same file). 425 | 426 | You also need to make sure that all the needed basic config values for logtodb is set by either: 427 | * Copy over logtodb.php from the config folder of this addon, 428 | * or just add all your log-to-db options in your applications config/logging.php file (probably easiest). Just follow the 429 | configuration example above under the [configuration](#configuration) section. 430 | 431 | Since we are using Lumen we need to specify the config and service providers in the "bootstrap/app.php" file. 432 | 433 | ``` 434 | /* 435 | |-------------------------------------------------------------------------- 436 | | Register Config Files 437 | |-------------------------------------------------------------------------- 438 | | 439 | | Now we will register the "app" configuration file. If the file exists in 440 | | your configuration directory it will be loaded; otherwise, we'll load 441 | | the default version. You may register other files below as needed. 442 | | 443 | */ 444 | 445 | $app->configure('app'); 446 | //$app->configure('logtodb'); //if you copied over and want to use the base config from logtodb. 447 | $app->configure('logging'); 448 | 449 | ``` 450 | 451 | Next step is to register the service provider, either in bootstrap/app.php or in app/Provider/AppServiceProvider. 452 | ``` 453 | /* 454 | |-------------------------------------------------------------------------- 455 | | Register Service Providers 456 | |-------------------------------------------------------------------------- 457 | | 458 | | Here we will register all of the application's service providers which 459 | | are used to bind services into the container. Service providers are 460 | | totally optional, so you are not required to uncomment this line. 461 | | 462 | */ 463 | 464 | // $app->register(App\Providers\AppServiceProvider::class); 465 | // $app->register(App\Providers\AuthServiceProvider::class); 466 | // $app->register(App\Providers\EventServiceProvider::class); 467 | $app->register(\danielme85\LaravelLogToDB\ServiceProvider::class); 468 | ``` 469 | 470 | After adding the service provider you should be able to run the database migration in Lumen with: 471 | ``` 472 | php artisan migrate --path=vendor/danielme85/laravel-log-to-db/src/migrations/2018_08_11_003343_create_log_table.php 473 | ``` 474 | Please note that you need a working db connection in Lumen at this point. 475 | 476 | And then maybe it works... ¯\_(ツ)_/¯ 477 | 478 | #### Using worker queue to write log to db with Lumen 479 | You would need to set up a queue driver in Lumen before you can use the queue (default is: QUEUE_CONNECTION=sync, which is basically no queue). 480 | More info about the [queues in Lumen doc](https://lumen.laravel.com/docs/7.x/queues) (they are mostly the same as Laravel). 481 | I would recommend the Redis queue driver but database should also work. 482 | 483 | #### How to make Redis work in Lumen (in general). 484 | install per command 485 | ``` 486 | composer require predis/predis 487 | composer require illuminate/redis 488 | ``` 489 | 490 | OR add to composer.json 491 | ``` 492 | ... 493 | "require": { 494 | "predis/predis": "~1.0", 495 | "illuminate/redis": "5.0.*", 496 | ... 497 | ``` 498 | Or install other alternatives to predis. 499 | 500 | Add service provider and enable eloquent in your bootstrap/app.php file (Eloquent only needed if you use the model/model helper class to fetch new log event); 501 | ``` 502 | 503 | $app->withFacades(); 504 | $app->withEloquent(); 505 | 506 | $app->register(Illuminate\Redis\RedisServiceProvider::class); 507 | if (!class_exists('Redis')) { 508 | class_alias('Illuminate\Support\Facades\Redis', 'Redis'); 509 | } 510 | ``` 511 | 512 | Now you can set your .env values to use Redis for the queue and cache if you so please: 513 | ``` 514 | REDIS_CLIENT=predis 515 | QUEUE_CONNECTION=redis 516 | CACHE_DRIVER=redis 517 | ``` 518 | 519 | ## Local Testing With Docker 520 | There is a helper bash script called 'runLocalTestInDocker.sh', that runs the following docker commands: 521 | ``` 522 | docker-compose up -d mariadb mongo && 523 | docker-compose up php7 && 524 | docker-compose up php8 && 525 | docker-compose down 526 | ``` 527 | To run Docker is required, give execute rights to the script and run: 528 | ``` 529 | chmod +x runLocalTestInDocker.sh && 530 | ./runLocalTestInDocker.sh 531 | ``` 532 | 533 | ### Development supported by: 534 | ![](https://media.giphy.com/media/3knKct3fGqxhK/giphy.gif) 535 | -------------------------------------------------------------------------------- /runLocalTestInDocker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker compose up -d logto-mariadb logto-mongodb && 3 | docker compose up php8 && 4 | docker compose down 5 | -------------------------------------------------------------------------------- /src/Commands/LogCleanerUpper.php: -------------------------------------------------------------------------------- 1 | getLogToDbChannels(); 50 | 51 | if (!empty($channels)) { 52 | foreach ($channels as $name => $channel) { 53 | $maxRecords = $channel['max_records'] ?? $config['purge_log_when_max_records'] ?? false; 54 | $maxHours = $channel['max_hours'] ?? $config['purge_log_when_max_records'] ?? false; 55 | 56 | //delete based on numbers of records 57 | if (!empty($maxRecords) && $maxRecords > 0) { 58 | if (LogToDB::model($name)->removeOldestIfMoreThan($maxRecords)) { 59 | $this->warn("Deleted oldest records on log channel: {$name}, keep max number of records: {$maxRecords}"); 60 | } 61 | } 62 | 63 | //delete based on age 64 | if (!empty($maxHours) && $maxHours > 0) { 65 | $time = Carbon::now()->subHours($maxHours)->toDateTimeString(); 66 | if (LogToDB::model($name)->removeOlderThan($time)) { 67 | $this->warn("Deleted oldest records on log channel: {$name}, older than: {$time}"); 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * @return array 76 | */ 77 | private function getLogToDbChannels() 78 | { 79 | $list = []; 80 | $logging = config('logging'); 81 | if (!empty($logging) && isset($logging['channels']) && !empty($logging['channels'])) { 82 | foreach ($logging['channels'] as $name => $channel) { 83 | //Only look for the relevant logging class in the config. 84 | if (isset($channel['via']) && $channel['via'] === LogToDbHandler::class) { 85 | $list[$name] = $channel; 86 | } 87 | } 88 | } 89 | 90 | return $list; 91 | } 92 | } -------------------------------------------------------------------------------- /src/Facades/LogToDB.php: -------------------------------------------------------------------------------- 1 | safeWrite($this->record); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/LogToDB.php: -------------------------------------------------------------------------------- 1 | config = $loggingConfig + (config('logtodb') ?? []); 55 | $this->collection = $this->config['collection'] ?? 'log'; 56 | $this->model = $this->config['model'] ?? null; 57 | 58 | //Get the DB connections 59 | $dbconfig = config('database.connections'); 60 | 61 | if (!empty($this->config['connection'])) { 62 | if (!empty($dbconfig[$this->config['connection']])) { 63 | $this->connection = $this->config['connection']; 64 | } 65 | } 66 | 67 | //set the actual connection instead of default 68 | if ($this->connection === 'default' or empty($this->connection)) { 69 | $this->connection = config('database.default'); 70 | } 71 | 72 | if (isset($dbconfig[$this->connection])) { 73 | $this->database = $dbconfig[$this->connection]; 74 | } 75 | } 76 | 77 | /** 78 | * Return a new LogToDB Module instance. 79 | * 80 | * If specifying 'channel, 'connection' and 'collection' would not be needed (they will be extracted from channel). 81 | * If specifying 'connection' and 'collection', 'channel' is not needed. 82 | * 83 | * @param string|null $channel 84 | * @param string|null $connection 85 | * @param string|null $collection 86 | * 87 | * @return DBLog|DBLogMongoDB 88 | */ 89 | public static function model(?string $channel = null, string $connection = 'default', ?string $collection = null) 90 | { 91 | $conn = null; 92 | $coll = null; 93 | 94 | if (!empty($channel)) { 95 | $channels = config('logging.channels'); 96 | if (!empty($channels[$channel])) { 97 | if (!empty($channels[$channel]['connection'])) { 98 | $conn = $channels[$channel]['connection']; 99 | } 100 | if (!empty($channels[$channel]['collection'])) { 101 | $coll = $channels[$channel]['collection']; 102 | } 103 | } 104 | } else { 105 | $conn = $connection; 106 | if (!empty($collection)) { 107 | $coll = $collection; 108 | } 109 | } 110 | 111 | //Return new instance of this model 112 | $model = new self(['connection' => $conn, 'collection' => $coll]); 113 | 114 | return $model->getModel(); 115 | } 116 | 117 | /** 118 | * @return DBLogMongoDB | DBLog; 119 | */ 120 | public function getModel() 121 | { 122 | //Use custom model 123 | if (!empty($this->model)) { 124 | return new $this->model; 125 | } else if (isset($this->database['driver']) && $this->database['driver'] === 'mongodb') { 126 | //MongoDB has its own Model 127 | $mongo = new DBLogMongoDB(); 128 | $mongo->bind($this->connection, $this->collection); 129 | 130 | return $mongo; 131 | } else { 132 | //Use the default Laravel Eloquent Model 133 | $sql = new DBLog(); 134 | $sql->bind($this->connection, $this->collection); 135 | 136 | return $sql; 137 | } 138 | } 139 | 140 | /** 141 | * Create a Eloquent Model 142 | * 143 | * @param \Monolog\LogRecord $record 144 | * @return bool success 145 | */ 146 | public function newFromMonolog(LogRecord $record) 147 | { 148 | $detailed = $this->getConfig('detailed'); 149 | 150 | if (!empty($this->connection)) { 151 | if ($detailed && !empty($record['context']) && !empty($record['context']['exception'])) { 152 | $record = new LogRecord( 153 | $record->datetime, 154 | $record->channel, 155 | $record->level, 156 | $record->message, 157 | self::parseIfException($record->context, true), 158 | $record->extra 159 | ); 160 | } else if (!$detailed) { 161 | $record = new LogRecord( 162 | $record->datetime, 163 | $record->channel, 164 | $record->level, 165 | $record->message, 166 | [], 167 | $record->extra 168 | ); 169 | } 170 | if (!empty($this->config['queue'])) { 171 | if (empty($this->config['queue_name']) && empty($this->config['queue_connection'])) { 172 | dispatch(new SaveNewLogEvent($record)); 173 | } else if (!empty($this->config['queue_name']) && !empty($this->config['queue_connection'])) { 174 | dispatch(new SaveNewLogEvent($record)) 175 | ->onConnection($this->config['queue_connection']) 176 | ->onQueue($this->config['queue_name']); 177 | } else if (!empty($this->config['queue_connection'])) { 178 | dispatch(new SaveNewLogEvent($record)) 179 | ->onConnection($this->config['queue_connection']); 180 | } else if (!empty($this->config['queue_name'])) { 181 | dispatch(new SaveNewLogEvent($record)) 182 | ->onQueue($this->config['queue_name']); 183 | } 184 | } else { 185 | $this->safeWrite($record); 186 | return true; 187 | } 188 | } 189 | 190 | return false; 191 | } 192 | 193 | public function safeWrite(LogRecord $record) { 194 | try { 195 | $model = $this->getModel(); 196 | $log = $model->generate( 197 | $record 198 | ); 199 | $log->save(); 200 | } catch (\Throwable $e) { 201 | self::emergencyLog(new LogRecord( 202 | datetime: new \Monolog\DateTimeImmutable(true), 203 | channel: '', 204 | level: \Monolog\Level::Critical, 205 | message: 'There was an error while trying to write the log to a DB, log record pushed to error_log()', 206 | context: LogToDB::parseIfException(['exception' => $e]), 207 | extra: [] 208 | )); 209 | 210 | self::emergencyLog($record); 211 | } 212 | } 213 | 214 | public static function emergencyLog(LogRecord $record) 215 | { 216 | $errorHandler = new ErrorLogHandler(); 217 | $errorHandler->setFormatter(new LineFormatter('%level_name%: %message% %context%')); 218 | $errorHandler->handle($record); 219 | } 220 | 221 | /** 222 | * Get config value 223 | * 224 | * @param string $config 225 | * @return mixed|null 226 | */ 227 | public function getConfig(string $config) 228 | { 229 | return $this->config[$config] ?? null; 230 | } 231 | 232 | /** 233 | * Parse the exception class 234 | * 235 | * @param array $context 236 | * @param bool $trace 237 | * @return array 238 | */ 239 | public static function parseIfException(array $context, bool $trace = false) 240 | { 241 | if (!empty($context['exception'])) { 242 | $exception = $context['exception']; 243 | if (is_object($exception)) { 244 | if (get_class($exception) === \Exception::class 245 | || get_class($exception) === \Throwable::class 246 | || is_subclass_of($exception, \Exception::class) 247 | || is_subclass_of($exception, \Throwable::class) 248 | || strpos(get_class($exception), "Exception") !== false 249 | || strpos(get_class($exception), "Throwable") !== false) { 250 | 251 | $newexception = []; 252 | 253 | if (method_exists($exception, 'getMessage')) { 254 | $newexception['message'] = $exception->getMessage(); 255 | } 256 | if (method_exists($exception, 'getCode')) { 257 | $newexception['code'] = $exception->getCode(); 258 | } 259 | if (method_exists($exception, 'getFile')) { 260 | $newexception['file'] = $exception->getFile(); 261 | } 262 | if (method_exists($exception, 'getLine')) { 263 | $newexception['line'] = $exception->getLine(); 264 | } 265 | if ($trace && method_exists($exception, 'getTraceAsString')) { 266 | $newexception['trace'] = $exception->getTraceAsString(); 267 | } 268 | if (method_exists($exception, 'getSeverity')) { 269 | $newexception['severity'] = $exception->getSeverity(); 270 | } 271 | 272 | $context['exception'] = $newexception; 273 | } 274 | } 275 | } 276 | 277 | return $context; 278 | } 279 | 280 | } 281 | -------------------------------------------------------------------------------- /src/LogToDbCustomLoggingHandler.php: -------------------------------------------------------------------------------- 1 | config = $config; 38 | 39 | //Set default level debug 40 | $level = $config['level'] ?? 'debug'; 41 | 42 | //Set the processors 43 | if (!empty($processors)) { 44 | foreach ($processors as $processor) { 45 | $this->pushProcessor($processor); 46 | } 47 | } 48 | 49 | parent::__construct($level, $bubble); 50 | } 51 | 52 | /** 53 | * Write the Log 54 | * 55 | * @param \Monolog\LogRecord $record 56 | * @throws DBLogException 57 | */ 58 | protected function write(LogRecord $record): void 59 | { 60 | try { 61 | $log = new LogToDB($this->config); 62 | $log->newFromMonolog($record); 63 | } catch (\Exception $e) { 64 | LogToDB::emergencyLog(new LogRecord( 65 | datetime: new \Monolog\DateTimeImmutable(true), 66 | channel: '', 67 | level: \Monolog\Level::Critical, 68 | message: 'There was an error while trying to write the log to a DB, log record pushed to error_log()', 69 | context: LogToDB::parseIfException(['exception' => $e]), 70 | extra: [] 71 | )); 72 | 73 | LogToDB::emergencyLog($record); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/LogToDbHandler.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 3/5/2019 5 | * Time: 10:42 PM 6 | * 7 | * Source: https://stackoverflow.com/a/45914381 8 | */ 9 | 10 | namespace danielme85\LaravelLogToDB\Models; 11 | 12 | trait BindsDynamically 13 | { 14 | protected $connection = null; 15 | protected $table = null; 16 | 17 | public function bind(string $connection, string $table) 18 | { 19 | $this->setConnection($connection); 20 | $this->setTable($table); 21 | } 22 | 23 | public function newInstance($attributes = [], $exists = false) 24 | { 25 | // Overridden in order to allow for late table binding. 26 | 27 | $model = parent::newInstance($attributes, $exists); 28 | $model->setTable($this->table); 29 | 30 | return $model; 31 | } 32 | } -------------------------------------------------------------------------------- /src/Models/DBLog.php: -------------------------------------------------------------------------------- 1 | message = $record->message; 24 | $this->context = $record->context; 25 | $this->level = $record->level->value; 26 | $this->level_name = $record->level->getName(); 27 | $this->channel = $record->channel; 28 | $this->datetime = $record->datetime; 29 | $this->extra = $record->extra; 30 | $this->unix_time = time(); 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * Context Accessor 37 | * 38 | * @param $value 39 | * @return null|array 40 | */ 41 | public function getContextAttribute($value) 42 | { 43 | return $this->jsonDecodeIfNotEmpty($value); 44 | } 45 | 46 | /** 47 | * Extra Accessor 48 | * 49 | * @param $value 50 | * @return null|array 51 | */ 52 | public function getExtraAttribute($value) 53 | { 54 | return $this->jsonDecodeIfNotEmpty($value); 55 | } 56 | 57 | /** 58 | * DateTime Mutator 59 | * 60 | * @param object $value 61 | */ 62 | public function setDatetimeAttribute(object $value) 63 | { 64 | $this->attributes['datetime'] = $value->format(config('logtodb.datetime_format')); 65 | } 66 | 67 | /** 68 | * Context Mutator 69 | * 70 | * @param array $value 71 | */ 72 | public function setContextAttribute($value) 73 | { 74 | $this->attributes['context'] = $this->jsonEncodeIfNotEmpty($value); 75 | } 76 | 77 | /** 78 | * Extra Mutator 79 | * 80 | * @param array $value 81 | */ 82 | public function setExtraAttribute($value) 83 | { 84 | $this->attributes['extra'] = $this->jsonEncodeIfNotEmpty($value); 85 | } 86 | 87 | /** 88 | * Encode to json if not empty/null 89 | * 90 | * @param $value 91 | * @return string 92 | */ 93 | private function jsonEncodeIfNotEmpty($value) 94 | { 95 | if (!empty($value)) { 96 | return @json_encode($value) ?? null; 97 | } 98 | } 99 | 100 | /** 101 | * Decode from json if not empty/null 102 | * 103 | * @param $value 104 | * @param bool $arraymode 105 | * @return mixed 106 | */ 107 | private function jsonDecodeIfNotEmpty($value, $arraymode = true) 108 | { 109 | if (!empty($value)) { 110 | return json_decode($value, $arraymode); 111 | } 112 | 113 | return $value; 114 | } 115 | 116 | /** 117 | * Delete the oldest records based on unix_time, silly spelling version. 118 | * 119 | * @param int $max amount of records to keep 120 | * @return bool 121 | */ 122 | public function removeOldestIfMoreThen(int $max) 123 | { 124 | return $this->removeOldestIfMoreThan($max); 125 | } 126 | 127 | /** 128 | * Delete the oldest records based on unix_time 129 | * 130 | * @param int $max amount of records to keep 131 | * @return bool success 132 | */ 133 | public function removeOldestIfMoreThan(int $max) 134 | { 135 | $current = $this->count(); 136 | if ($current > $max) { 137 | $keepers = $this->orderBy('unix_time', 'DESC')->take($max)->pluck($this->primaryKey)->toArray(); 138 | if ($this->whereNotIn($this->primaryKey, $keepers)->delete()) { 139 | return true; 140 | } 141 | } 142 | 143 | return false; 144 | } 145 | 146 | /** 147 | * Delete records based on date, silly spelling version. 148 | * 149 | * @param string $datetime date supported by strtotime: http://php.net/manual/en/function.strtotime.php 150 | * @return bool success 151 | */ 152 | public function removeOlderThen(string $datetime) 153 | { 154 | return $this->removeOlderThan($datetime); 155 | } 156 | 157 | /** 158 | * Delete records based on date. 159 | * 160 | * @param string $datetime date supported by strtotime: http://php.net/manual/en/function.strtotime.php 161 | * @return bool success 162 | */ 163 | public function removeOlderThan(string $datetime) 164 | { 165 | $unixtime = strtotime($datetime); 166 | $count = $this->where('unix_time', '<=', $unixtime)->delete(); 167 | 168 | return $count > 0; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Processors/AuthenticatedUserProcessor.php: -------------------------------------------------------------------------------- 1 | basePath() . '/config' . ($path ? '/' . $path : $path); 35 | } 36 | } 37 | 38 | //Merge config first, then keep a publish option 39 | $this->mergeConfigFrom(__DIR__.'/config/logtodb.php', 'logtodb'); 40 | $this->publishes([ 41 | __DIR__.'/config/logtodb.php' => config_path('logtodb.php'), 42 | ], 'config'); 43 | 44 | //Publish the migration 45 | $this->publishes([ 46 | __DIR__.'/migrations/' => database_path('migrations') 47 | ], 'migrations'); 48 | 49 | if ($this->app->runningInConsole()) { 50 | $this->commands([ 51 | LogCleanerUpper::class, 52 | ]); 53 | } 54 | } 55 | 56 | /** 57 | * Register the application services. 58 | * 59 | * @return void 60 | */ 61 | public function register() 62 | { 63 | $this->app->bind('laravel-log-to-db', function() { 64 | return new LogToDB(); 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/config/logtodb.php: -------------------------------------------------------------------------------- 1 | 'mysql' wil use the connection 'mysql' in database.php. 22 | | Ex: 'connection' => 'mongodb' wil use the connection 'mongodb' in database.php* 23 | | 24 | | Supported connections should be same as Laravel since the Laravel DB/Eloquent 25 | | Supported DB engines as of this writing: [MySQL] [PostgreSQL] [SQLite] [SQL Server] 26 | | 27 | | *MongoDB is supported with: "jenssegers/laravel-mongodb". 28 | | https://github.com/jenssegers/laravel-mongodb 29 | | laravel-mongodb is required to use the mongodb option for logging. 30 | */ 31 | 'connection' => env('LOG_DB_CONNECTION', ''), 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | DB Collection 36 | |-------------------------------------------------------------------------- 37 | | 38 | | Set the default database table (sql) or collection (mongodb) to use. 39 | */ 40 | 'collection' => env('LOG_DB_COLLECTION', 'log'), 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Detailed log 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Set detailed log. Detailed log means the inclusion of a context (stack trace). 48 | | This will usually require quite a bit more DB storage space, and is probably 49 | | only useful in development/debugging. You can still have this enabled in production 50 | | environments if more detailed error logs are proffered. 51 | */ 52 | 'detailed' => env('LOG_DB_DETAILED', true), 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Model class 57 | |-------------------------------------------------------------------------- 58 | | 59 | | You can specify your own custom Eloquent model to be used when saving 60 | | and getting log events. 61 | */ 62 | 'model' => env('LOG_DB_MODEL', false), 63 | 64 | /* 65 | |-------------------------------------------------------------------------- 66 | | Enable Queue 67 | |-------------------------------------------------------------------------- 68 | | 69 | | It might be a good idea to save log events with the queue helper. 70 | | This way the requests going to your sever does not have to wait for the Log 71 | | event to be saved. 72 | */ 73 | 'queue' => env('LOG_DB_QUEUE', false), 74 | 75 | /* 76 | |-------------------------------------------------------------------------- 77 | | Name of Queue 78 | |-------------------------------------------------------------------------- 79 | | 80 | | Set to a string like: 'queue_db_name' => 'logWorker', 81 | | and make sure to run the queue worker. Leave empty for default queue. 82 | */ 83 | 'queue_name' => env('LOG_DB_QUEUE_NAME', ''), 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Queue Connection 88 | |-------------------------------------------------------------------------- 89 | | 90 | | If you are working with multiple queue connections, you may specify which 91 | | connection to push a job to. 92 | | This relates yto your queue settings in the config/queue.php file. 93 | | Leave blank to use the default connection. 94 | | 95 | */ 96 | 'queue_connection' => env('LOG_DB_QUEUE_CONNECTION', ''), 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | Log record purging 101 | |-------------------------------------------------------------------------- 102 | | 103 | | Automatically purge db log records based on time and/or max number of records. 104 | | 105 | | Number of records: should be an integer that represents the max number of records 106 | | stored before the oldest is removed. 107 | | When older than: records older than a give number of hours will be removed. 108 | | 109 | */ 110 | 'max_records' => env('LOG_DB_MAX_COUNT', false), //Ex: 1000 records 111 | 112 | 'max_hours' => env('LOG_DB_MAX_HOURS', false), //Ex: 24 for 24 hours. Or 24*7 = 1 week. 113 | 114 | /* 115 | | 116 | | Specify the datetime format storing into the log record 117 | | 118 | */ 119 | 'datetime_format' => env('LOG_DB_DATETIME_FORMAT', 'Y-m-d H:i:s:ms') 120 | 121 | ]; 122 | -------------------------------------------------------------------------------- /src/migrations/2018_08_11_003343_create_log_table.php: -------------------------------------------------------------------------------- 1 | hasTable(config('logtodb.collection')) === false) { 17 | Schema::connection(config('logtodb.connection'))->create(config('logtodb.collection'), function (Blueprint $table) { 18 | $table->increments('id'); 19 | $table->text('message')->nullable(); 20 | $table->string('channel')->nullable(); 21 | $table->integer('level')->default(0); 22 | $table->string('level_name', 20); 23 | $table->integer('unix_time'); 24 | $table->string('datetime')->nullable(); 25 | $table->longText('context')->nullable(); 26 | $table->text('extra')->nullable(); 27 | $table->timestamps(); 28 | }); 29 | } 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | * @return void 36 | */ 37 | public function down() 38 | { 39 | Schema::connection(config('logtodb.connection'))->dropIfExists(config('logtodb.collection')); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /testbench.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | - DB_CONNECTION=mysql 3 | - DB_HOST=mariadb 4 | - DB_PORT=3306 5 | - DB_DATABASE=logtodb 6 | - DB_USERNAME=root 7 | - MDB_DATABASE=logtodb 8 | - MDB_HOST=mongo 9 | - MDB_PORT=27017 10 | 11 | providers: 12 | - danielme85\LaravelLogToDB\ServiceProvider 13 | -------------------------------------------------------------------------------- /tests/FailureTest.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 9/28/2021 5 | * Time: 4:29 AM 6 | */ 7 | 8 | use Illuminate\Support\Facades\Log; 9 | 10 | class FailureTest extends Orchestra\Testbench\TestCase 11 | { 12 | 13 | protected function getEnvironmentSetUp($app) 14 | { 15 | $dotenv = Dotenv\Dotenv::createImmutable(__DIR__.'/../', '.env.testing'); 16 | $dotenv->load(); 17 | 18 | $app['config']->set('logging.default', 'database'); 19 | $app['config']->set('logging.channels', [ 20 | 'database' => [ 21 | 'driver' => 'custom', 22 | 'via' => danielme85\LaravelLogToDB\LogToDbHandler::class, 23 | 'level' => 'debug', 24 | 'connection' => 'default', 25 | 'collection' => 'log', 26 | 'max_records' => 10, 27 | 'max_hours' => 1, 28 | 'processors' => [ 29 | Monolog\Processor\HostnameProcessor::class, 30 | Monolog\Processor\MemoryUsageProcessor::class, 31 | danielme85\LaravelLogToDB\Processors\PhpVersionProcessor::class 32 | ] 33 | ], 34 | ]); 35 | 36 | $app['config']->set('logtodb', include __DIR__.'/../src/config/logtodb.php'); 37 | } 38 | 39 | 40 | 41 | 42 | public function testEmergencyFailure() 43 | { 44 | $capture = tmpfile(); 45 | $backup = ini_set('error_log', stream_get_meta_data($capture)['uri']); 46 | 47 | Log::info('what?'); 48 | $result = stream_get_contents($capture); 49 | 50 | ini_set('error_log', $backup); 51 | 52 | $this->assertStringContainsString( 53 | 'CRITICAL: There was an error while trying to write the log to a DB', 54 | $result 55 | ); 56 | $this->assertStringContainsString( 57 | 'INFO: what?', 58 | $result 59 | ); 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /tests/LogToDbTest.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__.'/../src/migrations'); 29 | } 30 | 31 | /** 32 | * Define database migrations. 33 | * 34 | * @return void 35 | */ 36 | protected function defineDatabaseMigrations() 37 | { 38 | $this->artisan('migrate', ['--database' => 'mysql'])->run(); 39 | } 40 | 41 | /** 42 | * Define environment setup. 43 | * 44 | * @param \Illuminate\Foundation\Application $app 45 | * 46 | * @return void 47 | */ 48 | protected function defineEnvironment($app) 49 | { 50 | $dotenv = Dotenv::createImmutable(__DIR__.'/../', '.env.testing'); 51 | $dotenv->load(); 52 | 53 | $app['config']->set('database.default', 'mysql'); 54 | $app['config']->set('database.connections', 55 | ['mysql' => [ 56 | 'driver' => 'mysql', 57 | 'host' => env('DB_HOST', '127.0.0.1'), 58 | 'port' => env('DB_PORT', 3306), 59 | 'database' => env('DB_DATABASE', 'testing'), 60 | 'username' => env('DB_USERNAME', 'root'), 61 | 'password' => env('DB_PASSWORD', ''), 62 | 'charset' => 'utf8', 63 | 'collation' => 'utf8_unicode_ci', 64 | ], 65 | 'mongodb' => [ 66 | 'driver' => 'mongodb', 67 | 'host' => env('MDB_HOST', '127.0.0.1'), 68 | 'port' => env('MDB_PORT', 27017), 69 | 'database' => env('MDB_DATABASE', 'testing'), 70 | 'username' => env('MDB_USER', ''), 71 | 'password' => env('MDB_PASSWORD', ''), 72 | 'options' => [ 73 | 'database' => 'admin' // sets the authentication database required by mongo 3 74 | ] 75 | ], 76 | ]); 77 | 78 | $app['config']->set('logging.default', 'stack'); 79 | $app['config']->set('logging.channels', [ 80 | 'stack' => [ 81 | 'driver' => 'stack', 82 | 'channels' => ['database', 'mongodb'], 83 | ], 84 | 'database' => [ 85 | 'driver' => 'custom', 86 | 'via' => LogToDbHandler::class, 87 | 'level' => 'debug', 88 | 'connection' => 'default', 89 | 'collection' => 'log', 90 | 'max_records' => 10, 91 | 'max_hours' => 1, 92 | 'processors' => [ 93 | HostnameProcessor::class, 94 | MemoryUsageProcessor::class, 95 | PhpVersionProcessor::class 96 | ] 97 | ], 98 | 'mongodb' => [ 99 | 'driver' => 'custom', 100 | 'via' => LogToDbHandler::class, 101 | 'level' => 'debug', 102 | 'connection' => 'mongodb', 103 | 'collection' => 'log', 104 | 'max_records' => 10, 105 | 'max_hours' => 1, 106 | 'processors' => [ 107 | HostnameProcessor::class 108 | ] 109 | ], 110 | 'limited' => [ 111 | 'driver' => 'custom', 112 | 'via' => LogToDbHandler::class, 113 | 'level' => 'warning', 114 | 'detailed' => false, 115 | 'max_records' => false, 116 | 'max_hours' => false, 117 | 'name' => 'limited', 118 | ] 119 | ]); 120 | 121 | $app['config']->set('logtodb', include __DIR__.'/../src/config/logtodb.php'); 122 | } 123 | 124 | /** 125 | * Get package providers. At a minimum this is the package being tested, but also 126 | * would include packages upon which our package depends, e.g. Cartalyst/Sentry 127 | * In a normal app environment these would be added to the 'providers' array in 128 | * the config/app.php8 file. 129 | * 130 | * @param \Illuminate\Foundation\Application $app 131 | * 132 | * @return array 133 | */ 134 | protected function getPackageProviders($app) 135 | { 136 | return [ 137 | \danielme85\LaravelLogToDB\ServiceProvider::class, 138 | \MongoDB\Laravel\MongoDBServiceProvider::class, 139 | ]; 140 | } 141 | 142 | /** 143 | * Basic test to see if class can be instanced. 144 | * 145 | * @group basic 146 | */ 147 | public function testClassInit() 148 | { 149 | $this->assertInstanceOf(LogToDB::class, app('laravel-log-to-db')); 150 | $this->assertInstanceOf(LogToDB::class, new LogToDB()); 151 | 152 | //Class works, now lets cleanup possible failed test 153 | LogToDB::model()->truncate(); 154 | LogToDB::model('mongodb')->truncate(); 155 | } 156 | 157 | /** 158 | * Run basic log levels 159 | * 160 | * @group basic 161 | */ 162 | public function testLogLevels() 163 | { 164 | Log::debug("This is an test DEBUG log event"); 165 | Log::info("This is an test INFO log event"); 166 | Log::notice("This is an test NOTICE log event"); 167 | Log::warning("This is an test WARNING log event"); 168 | Log::error("This is an test ERROR log event"); 169 | Log::critical("This is an test CRITICAL log event"); 170 | Log::alert("This is an test ALERT log event"); 171 | Log::emergency("This is an test EMERGENCY log event"); 172 | 173 | //Check mysql 174 | $logReader = LogToDB::model()->get()->toArray(); 175 | $logReaderMongoDB = LogToDB::model('mongodb')->get()->toArray(); 176 | $logReaderSpecific = LogToDB::model('database', 'mysql', 'LogSql')->get()->toArray(); 177 | $this->assertCount(8, $logReader); 178 | $this->assertCount(8, $logReaderMongoDB); 179 | $this->assertCount(8, $logReaderSpecific); 180 | } 181 | 182 | /** 183 | * @group context 184 | */ 185 | public function testContext() 186 | { 187 | Log::error("Im trying to add some context", ['whatDis?' => 'dis some context, should always be array']); 188 | $log = LogToDB::model()->where('message', '=' , 'Im trying to add some context')->first(); 189 | $this->assertNotEmpty($log); 190 | $this->assertStringContainsString("Im trying to add some context", $log->message); 191 | $this->assertIsArray($log->context); 192 | } 193 | 194 | /** 195 | * Check to see if processors are adding extra content. 196 | * 197 | * @group basic 198 | */ 199 | public function testProcessors() 200 | { 201 | Log::info("hello"); 202 | $log = LogToDB::model()->orderBy('created_at', 'desc')->first()->toArray(); 203 | $this->assertNotEmpty($log['extra']); 204 | $this->assertNotEmpty($log['extra']['memory_usage']); 205 | $this->assertNotEmpty($log['extra']['php_version']); 206 | $this->assertNotEmpty($log['extra']['hostname']); 207 | } 208 | 209 | /** 210 | * Test logging to specific channels 211 | * 212 | * @group advanced 213 | */ 214 | public function testLoggingToChannels() 215 | { 216 | //Test limited config, with limited rows and level 217 | Log::channel('limited')->debug("This message should not be stored because DEBUG is LOWER then WARNING"); 218 | $this->assertEmpty(LogToDB::model('limited')->where('channel', 'limited')->where('level_name', 'DEBUG')->get()); 219 | 220 | //Test limited config, with limited rows and level 221 | Log::channel('limited')->warning("This message should be stored because WARNING = WARNING"); 222 | $this->assertNotEmpty(LogToDB::model('limited')->where('channel', 'limited')->where('level_name', 'WARNING')->get()); 223 | 224 | } 225 | 226 | /** 227 | * Test an exception error. 228 | * 229 | * @group exception 230 | */ 231 | public function testException() 232 | { 233 | $e = new BadRequestHttpException("This is a fake 500 error", null, 500, ['fake-header' => 'value']); 234 | Log::warning("Error", ['exception' => $e, 'more' => 'infohere']); 235 | $log = LogToDB::model()->where('message', 'Error')->first(); 236 | $this->assertNotEmpty($log->context); 237 | 238 | $empty = new \Mockery\Exception(); 239 | Log::warning("Error", ['exception' => $empty]); 240 | $log = LogToDB::model()->where('message', 'Error')->orderBy('id', 'DESC')->first(); 241 | $this->assertNotEmpty($log); 242 | 243 | $this->expectException(DBLogException::class); 244 | throw new DBLogException('Dont log this'); 245 | } 246 | 247 | /** 248 | * Test exception when expected format is wrong. 249 | * 250 | * @group exception 251 | */ 252 | public function testExceptionWrongFormat() 253 | { 254 | $e = [ 255 | 'message' => 'Array instead exception', 256 | 'code' => 0, 257 | 'file' => __FILE__, 258 | 'line' => __LINE__, 259 | 'trace' => debug_backtrace(), 260 | ]; 261 | Log::warning("Error", ['exception' => $e, 'more' => 'infohere']); 262 | $log = LogToDB::model()->where('message', 'Error')->first(); 263 | $this->assertNotEmpty($log->context); 264 | } 265 | 266 | /** 267 | * 268 | * @group exception 269 | */ 270 | public function testExceptionIgnore() 271 | { 272 | $this->assertCount(0, LogToDB::model()->where('message', '=', 'Dont log this')->get()->toArray()); 273 | } 274 | 275 | /** 276 | * Test queuing the log events. 277 | * 278 | * @group queue 279 | */ 280 | public function testQueue() 281 | { 282 | Queue::fake(); 283 | 284 | config()->set('logtodb.queue', true); 285 | 286 | Log::info("I'm supposed to be added to the queue..."); 287 | Log::warning("I'm supposed to be added to the queue..."); 288 | Log::debug("I'm supposed to be added to the queue..."); 289 | 290 | Queue::assertPushed(SaveNewLogEvent::class, 6); 291 | 292 | config()->set('logtodb.queue_name', 'logHandler'); 293 | 294 | Log::info("I'm supposed to be added to the queue..."); 295 | Log::warning("I'm supposed to be added to the queue..."); 296 | Log::debug("I'm supposed to be added to the queue..."); 297 | 298 | config()->set('logtodb.queue_name', null); 299 | config()->set('logtodb.queue_connection', 'default'); 300 | 301 | Log::info("I'm supposed to be added to the queue..."); 302 | Log::warning("I'm supposed to be added to the queue..."); 303 | Log::debug("I'm supposed to be added to the queue..."); 304 | 305 | config()->set('logtodb.queue_name', 'logHandler'); 306 | 307 | Log::info("I'm supposed to be added to the queue..."); 308 | Log::warning("I'm supposed to be added to the queue..."); 309 | Log::debug("I'm supposed to be added to the queue..."); 310 | 311 | Queue::assertPushed(SaveNewLogEvent::class, 24); 312 | 313 | config()->set('logtodb.queue', false); 314 | 315 | } 316 | 317 | /** 318 | * Test save new log event job 319 | * 320 | * @group job 321 | */ 322 | public function testSaveNewLogEventJob() 323 | { 324 | $logToDb = new LogToDB(); 325 | $record = new LogRecord( 326 | datetime: new \Monolog\DateTimeImmutable(true), 327 | channel: 'local', 328 | level: \Monolog\Level::Info, 329 | message: 'job-test', 330 | context: [], 331 | extra: [], 332 | formatted: "[2019-10-04T17:26:38.446827+00:00] local.INFO: test [] []\n", 333 | ); 334 | 335 | $job = new SaveNewLogEvent($record); 336 | $job->handle(); 337 | 338 | $this->assertNotEmpty($logToDb->model()->where('message', '=', 'job-test')->get()); 339 | } 340 | 341 | 342 | /** 343 | * Test model interaction 344 | * 345 | * @group model 346 | */ 347 | public function testModelInteraction() 348 | { 349 | LogToDB::model()->truncate(); 350 | LogToDB::model('mongodb')->truncate(); 351 | 352 | for ($i=1; $i<=10; $i++) { 353 | Log::debug("This is debug log message..."); 354 | } 355 | for ($i=1; $i<=10; $i++) { 356 | Log::info("This is info log message..."); 357 | } 358 | 359 | 360 | $model = LogToDB::model(); 361 | 362 | $log = $model->first(); 363 | $this->assertIsNumeric($log->id); 364 | $this->assertIsString($log->message); 365 | $this->assertIsString($log->channel); 366 | $this->assertIsNumeric($log->level); 367 | $this->assertIsString($log->level_name); 368 | $this->assertIsNumeric($log->unix_time); 369 | $this->assertIsString($log->datetime); 370 | $this->assertIsArray($log->extra); 371 | $this->assertNotEmpty($log->created_at); 372 | $this->assertNotEmpty($log->updated_at); 373 | $this->assertDatabaseCount('log', 20); 374 | 375 | 376 | //Get all 377 | $all = $model->get(); 378 | $this->assertNotEmpty($all->toArray()); 379 | //Get Debug 380 | $logs = $model->where('level_name', '=', 'DEBUG')->get()->toArray(); 381 | $this->assertNotEmpty($logs); 382 | $this->assertEquals('DEBUG', $logs[0]['level_name']); 383 | $this->assertCount(10, $logs); 384 | 385 | $model = LogToDB::model('database'); 386 | //Get all 387 | $all = $model->get(); 388 | $this->assertNotEmpty($all->toArray()); 389 | //Get Debug 390 | $logs = $model->where('level_name', '=', 'DEBUG')->get()->toArray(); 391 | $this->assertNotEmpty($logs); 392 | $this->assertEquals('DEBUG', $logs[0]['level_name']); 393 | $this->assertCount(10, $logs); 394 | 395 | 396 | $model = LogToDB::model(null, 'mysql'); 397 | //Get all 398 | $all = $model->get(); 399 | $this->assertNotEmpty($all->toArray()); 400 | //Get Debug 401 | $logs = $model->where('level_name', '=', 'DEBUG')->get()->toArray(); 402 | $this->assertNotEmpty($logs); 403 | $this->assertEquals('DEBUG', $logs[0]['level_name']); 404 | 405 | $model = LogToDB::model('database', 'mysql', 'log'); 406 | //Get all 407 | $all = $model->get(); 408 | $this->assertNotEmpty($all->toArray()); 409 | //Get Debug 410 | $logs = $model->where('level_name', '=', 'DEBUG')->get()->toArray(); 411 | $this->assertNotEmpty($logs); 412 | $this->assertEquals('DEBUG', $logs[0]['level_name']); 413 | $this->assertCount(10, $logs); 414 | 415 | //Same tests for mongoDB 416 | $modelMongo = LogToDB::model('mongodb'); 417 | //Get all 418 | $all = $modelMongo->get(); 419 | $this->assertNotEmpty($all->toArray()); 420 | //Get Debug 421 | $logs = $modelMongo->where('level_name', '=', 'DEBUG')->get()->toArray(); 422 | $this->assertNotEmpty($logs); 423 | $this->assertEquals('DEBUG', $logs[0]['level_name']); 424 | $this->assertCount(10, $logs); 425 | 426 | //Same tests for mongoDB 427 | $modelMongo = LogToDB::model('mongodb', 'mongodb', 'log'); 428 | //Get all 429 | $all = $modelMongo->get(); 430 | $this->assertNotEmpty($all->toArray()); 431 | //Get Debug 432 | $logs = $modelMongo->where('level_name', '=', 'DEBUG')->get()->toArray(); 433 | $this->assertNotEmpty($logs); 434 | $this->assertEquals('DEBUG', $logs[0]['level_name']); 435 | $this->assertCount(10, $logs); 436 | 437 | //Same tests for mongoDB 438 | $modelMongo = LogToDB::model(null, 'mongodb'); 439 | //Get all 440 | $all = $modelMongo->get(); 441 | $this->assertNotEmpty($all->toArray()); 442 | //Get Debug 443 | $logs = $modelMongo->where('level_name', '=', 'DEBUG')->get()->toArray(); 444 | $this->assertNotEmpty($logs); 445 | $this->assertEquals('DEBUG', $logs[0]['level_name']); 446 | $this->assertCount(10, $logs); 447 | } 448 | 449 | /** 450 | * $group model 451 | */ 452 | public function testStandAloneModels() 453 | { 454 | Log::info("This is a info log message..."); 455 | 456 | $modelMongo = LogToDB::model(null, 'mongodb'); 457 | 458 | $this->assertNotEmpty(LogSql::get()->toArray()); 459 | $this->assertNotEmpty($modelMongo->get()->toArray()); 460 | } 461 | 462 | /** 463 | * @group model 464 | */ 465 | public function testCustomModel() 466 | { 467 | config()->set('logtodb.model', CustomEloquentModel::class); 468 | Log::info('This is on a custom model class'); 469 | $this->assertStringContainsString('This is on a custom model class', LogToDB::model()->latest('id')->first()->message); 470 | } 471 | 472 | /** 473 | * Test the cleanup functions. 474 | * 475 | * @group cleanup 476 | */ 477 | public function testRemoves() 478 | { 479 | $this->assertFalse(LogToDB::model()->removeOldestIfMoreThan(1000)); 480 | 481 | Log::debug("This is an test DEBUG log event"); 482 | Log::info("This is an test INFO log event"); 483 | Log::notice("This is an test NOTICE log event"); 484 | 485 | //sleep to pass time for record cleanup testing based on time next. 486 | sleep(1); 487 | 488 | $this->assertTrue(LogToDB::model()->removeOldestIfMoreThan(2)); 489 | $this->assertEquals(2, LogToDB::model()->count()); 490 | $this->assertTrue(LogToDB::model()->removeOlderThan(date('Y-m-d H:i:s'))); 491 | $this->assertEquals(0, LogToDB::model()->count()); 492 | 493 | //Same tests on mongodb 494 | $this->assertTrue(LogToDB::model('mongodb')->removeOldestIfMoreThan(2)); 495 | $this->assertEquals(2, LogToDB::model('mongodb')->count()); 496 | $this->assertTrue(LogToDB::model('mongodb')->removeOlderThan(date('Y-m-d H:i:s'))); 497 | $this->assertEquals(0, LogToDB::model('mongodb')->count()); 498 | 499 | 500 | //test wrappers for silly spelling 501 | Log::debug("This is an test DEBUG log event"); 502 | Log::info("This is an test INFO log event"); 503 | Log::notice("This is an test NOTICE log event"); 504 | 505 | //sleep to pass time for record cleanup testing based on time next. 506 | sleep(1); 507 | 508 | $this->assertTrue(LogToDB::model()->removeOldestIfMoreThen(2)); 509 | $this->assertEquals(2, LogToDB::model()->count()); 510 | $this->assertTrue(LogToDB::model()->removeOlderThen(date('Y-m-d H:i:s'))); 511 | $this->assertEquals(0, LogToDB::model()->count()); 512 | 513 | //Same tests on mongodb 514 | $this->assertTrue(LogToDB::model('mongodb')->removeOldestIfMoreThen(2)); 515 | $this->assertEquals(2, LogToDB::model('mongodb')->count()); 516 | $this->assertTrue(LogToDB::model('mongodb')->removeOlderThen(date('Y-m-d H:i:s'))); 517 | $this->assertEquals(0, LogToDB::model('mongodb')->count()); 518 | 519 | } 520 | 521 | /** 522 | * Test the CleanerUpper 523 | * 524 | * @group cleanerUpper 525 | */ 526 | public function testCleanerUpper() 527 | { 528 | //Add bunch of old records 529 | for ($i = 0; $i < 20; $i++) { 530 | $log = LogToDB::model(); 531 | $thePast = \Carbon\Carbon::now()->subHours(24); 532 | 533 | $log->message = "This is fake test number: {$i}"; 534 | $log->channel = 'test'; 535 | $log->level = 100; 536 | $log->level_name = 'DEBUG'; 537 | $log->unix_time = $thePast->unix(); 538 | $log->datetime = new \Monolog\DateTimeImmutable(time()); 539 | $log->created_at = $thePast->toDateTimeString(); 540 | $log->updated_at = $thePast->toDateTimeString(); 541 | $log->save(); 542 | } 543 | 544 | //Add bunch of old records 545 | for ($i = 0; $i < 20; $i++) { 546 | $log = LogToDB::model('mongodb'); 547 | $thePast = \Carbon\Carbon::now()->subHours(24); 548 | 549 | $log->message = "This is fake test number: {$i}"; 550 | $log->channel = 'test'; 551 | $log->level = 100; 552 | $log->level_name = 'DEBUG'; 553 | $log->unix_time = $thePast->unix(); 554 | $log->datetime = new \Monolog\DateTimeImmutable(time()); 555 | $log->created_at = $thePast->toDateTimeString(); 556 | $log->updated_at = $thePast->toDateTimeString(); 557 | $log->save(); 558 | } 559 | 560 | //Add 5 new records 561 | for ($i = 0; $i < 5; $i++) { 562 | Log::debug("This is an test DEBUG log event number: {$i}"); 563 | } 564 | 565 | $this->assertEquals(25, LogToDB::model()->count()); 566 | $this->assertEquals(25, LogToDB::model('mongodb')->count()); 567 | 568 | //Run cleanup command 569 | $this->artisan('log:delete')->assertExitCode(0); 570 | 571 | $this->assertEquals(5, LogToDB::model()->count()); 572 | $this->assertEquals(5, LogToDB::model('mongodb')->count()); 573 | 574 | //Add 10 new records 575 | for ($i = 0; $i < 10; $i++) { 576 | Log::debug("This is an test DEBUG log event number: {$i}"); 577 | } 578 | 579 | //Run cleanup command 580 | $this->artisan('log:delete')->assertExitCode(0); 581 | 582 | $this->assertEquals(10, LogToDB::model()->count()); 583 | $this->assertEquals(10, LogToDB::model('mongodb')->count()); 584 | } 585 | 586 | /** 587 | * Clear all data from the test. 588 | * 589 | * @group cleanerUpper 590 | */ 591 | public function testFinalCleanup() 592 | { 593 | LogToDB::model()->truncate(); 594 | LogToDB::model('mongodb')->truncate(); 595 | 596 | $this->assertEmpty(LogToDB::model()->get()->toArray()); 597 | $this->assertEmpty(LogToDB::model('mongodb')->get()->toArray()); 598 | $this->assertEmpty(LogToDB::model('limited')->get()->toArray()); 599 | $this->assertEmpty(LogToDB::model('database')->get()->toArray()); 600 | 601 | $this->artisan('migrate:rollback', ['--database' => 'mysql']); 602 | } 603 | } 604 | -------------------------------------------------------------------------------- /tests/TestModels/CustomEloquentModel.php: -------------------------------------------------------------------------------- 1 |