🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,71 @@
name: Laravel Log Cleaner Tests
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [ '8.1', '8.2', '8.3' ]
laravel: [ '9.*', '10.*', '11.*', '12.*' ]
exclude:
# Laravel 11 requires PHP 8.2+
- php: '8.1'
laravel: '11.*'
# Laravel 12 requires PHP 8.2+
- php: '8.1'
laravel: '12.*'
name: P${{ matrix.php }} - L${{ matrix.laravel }}
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite
coverage: none
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update
composer update --prefer-dist --no-interaction --no-progress
- name: Create Laravel Project
run: |
composer create-project --prefer-dist laravel/laravel:${{ matrix.laravel }} test-app
cd test-app
composer require jiordiviera/laravel-log-cleaner
- name: Generate Log Files
run: |
cd test-app
php artisan tinker --execute="Log::info('Test log entry');"
php artisan tinker --execute="Log::error('Test error entry');"
- name: Run Log Cleaner
run: |
cd test-app
php artisan log:clear
- name: Check Log Files
run: |
cd test-app/storage/logs
if [ -s laravel.log ]; then
echo "Log file is not empty"
exit 1
else
echo "Log file is empty or does not exist"
fi
- name: Run Package Tests
run: vendor/bin/pest
@@ -0,0 +1,5 @@
vendor/
.phpunit.result.cache
.idea
.phpunit.cache/
composer.lock
@@ -0,0 +1,166 @@
# Changelog
All notable changes to `laravel-log-cleaner` will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Display estimated disk space to be freed in dry-run mode
- Validation for zlib extension before compression operations
- Enhanced validation for custom regex patterns
- Comprehensive test suite with 26 tests, 81 assertions
### Improved
- Dry-run output now shows both line count and estimated space (MB/GB)
- Better error messages for invalid regex patterns
- Enhanced test coverage for all features including compression
- Improved edge case handling for empty files, whitespace, and multiline logs
### Fixed
- Dry-run mode now properly suppresses non-dry-run messages
- Pattern validation correctly rejects invalid regex with helpful error messages
- Improved handling of empty log files and files with only whitespace
- Better management of backup/compress file naming conflicts
## [2.0.0] - 2025-01-18
### Added
- **Dry-run mode** (`--dry-run`): Preview changes without modifying files
- **Backup creation** (`--backup`): Create timestamped backups before cleaning
- Format: `laravel.log.backup.YYYY-MM-DD-HH-MM-SS`
- Automatic conflict resolution for multiple backups
- **Log level filtering** (`--level=LEVEL`): Keep only specific log levels
- Supported: EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG
- Preserves multi-line stack traces
- Combines with date filtering
- **Custom date patterns** (`--pattern=REGEX`): Support for non-standard log formats
- Custom regex pattern matching
- Flexible date extraction
- **Compression support** (`--compress`): Archive old logs instead of deleting
- Creates `.gz` compressed archives
- Format: `laravel.log.old.YYYY-MM-DD-HH-MM-SS.gz`
- Maximum compression level (level 9)
- **Memory-efficient processing** (`--memory-efficient`): Handle large log files
- Stream processing for files >50MB
- Automatic threshold detection
- Prevents out-of-memory errors
### Changed
- **BREAKING**: Minimum PHP version now 8.1+ (dropped PHP 7.x support)
- **BREAKING**: Minimum Laravel version now 9.x+ (dropped Laravel 7.x-8.x support)
- Enhanced error handling with detailed messages
- Improved file permission validation before operations
- Better handling of concurrent file access
- Optimized regex pattern compilation and caching
### Performance
- 50%+ performance improvement on large files (>100MB)
- Handles 1GB+ log files without memory issues
- Zero memory leaks with proper resource management
- Concurrent processing support for multiple log files
### Security
- Pre-flight permission validation
- Safe handling of invalid regex patterns
- Protection against path traversal
- Secure temporary file handling
### Testing
- Comprehensive test suite covering all features
- Performance benchmarks
- Memory usage validation
- Edge case coverage
## [1.0.4] - 2024-12-15
### Fixed
- Minor bug fixes and improvements
- Updated dependencies
## [1.0.3] - 2024-12-10
### Fixed
- Compatibility fixes for Laravel 11
- Improved error handling
## [1.0.2] - 2024-11-20
### Added
- Support for Laravel 11.x
- Improved documentation
### Fixed
- Minor bug fixes
## [1.0.1] - 2024-10-15
### Fixed
- Bug fixes and stability improvements
- Documentation updates
## [1.0.0] - 2024-09-01
### Added
- Initial release
- Basic log clearing functionality
- `--days` option to keep recent logs
- Support for Laravel 7.x, 8.x, 9.x, 10.x
- Support for PHP 7.0+
---
## Migration Guides
### From v1.x to v2.0
#### Requirements
- PHP 8.1 or higher
- Laravel 9.x or higher
#### Breaking Changes
1. **PHP Version**: Minimum PHP 8.1 required
2. **Laravel Version**: Minimum Laravel 9.x required
#### Upgrade Steps
1. Update `composer.json`:
```bash
composer require jiordiviera/laravel-log-cleaner:^2.0
```
2. Ensure PHP 8.1+ and Laravel 9.x+ are installed
3. No configuration changes needed - all new features are optional
#### Backwards Compatibility
Basic usage remains unchanged:
```bash
# These work identically in v1.x and v2.x
php artisan log:clear
php artisan log:clear --days=30
```
New features are opt-in:
```bash
# New v2.x features
php artisan log:clear --days=30 --backup
php artisan log:clear --days=30 --compress
php artisan log:clear --level=ERROR --dry-run
```
### Staying on v1.x
For older PHP or Laravel versions, continue using v1.x:
```bash
composer require jiordiviera/laravel-log-cleaner:^1.0
```
**v1.x Support**: Security fixes only until 2026-01-01
---
For detailed information about each release, see the [releases page](https://github.com/jiordiviera/laravel-log-cleaner/releases).
@@ -0,0 +1,7 @@
# The license
Copyright (c) Jiordi Viera <jiordikengne@gmail.com
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
@@ -0,0 +1,244 @@
```
██╗ █████╗ ██████╗ █████╗ ██╗ ██╗███████╗██╗
██║ ██╔══██╗██╔══██╗██╔══██╗██║ ██║██╔════╝██║
██║ ███████║██████╔╝███████║██║ ██║█████╗ ██║
██║ ██╔══██║██╔══██╗██╔══██║╚██╗ ██╔╝██╔══╝ ██║
███████╗██║ ██║██║ ██║██║ ██║ ╚████╔╝ ███████╗███████╗
╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚══════╝
██╗ ██████╗ ██████╗
██║ ██╔═══██╗██╔════╝
██║ ██║ ██║██║ ███╗
██║ ██║ ██║██║ ██║
███████╗╚██████╔╝╚██████╔╝
╚══════╝ ╚═════╝ ╚═════╝
██████╗██╗ ███████╗ █████╗ ███╗ ██╗███████╗██████╗
██╔════╝██║ ██╔════╝██╔══██╗████╗ ██║██╔════╝██╔══██╗
██║ ██║ █████╗ ███████║██╔██╗ ██║█████╗ ██████╔╝
██║ ██║ ██╔══╝ ██╔══██║██║╚██╗██║██╔══╝ ██╔══██╗
╚██████╗███████╗███████╗██║ ██║██║ ╚████║███████╗██║ ██║
╚═════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝
```
<p align="center">
<a href="https://packagist.org/packages/jiordiviera/laravel-log-cleaner"><img src="https://img.shields.io/packagist/v/jiordiviera/laravel-log-cleaner?style=for-the-badge" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/jiordiviera/laravel-log-cleaner"><img src="https://img.shields.io/packagist/dt/jiordiviera/laravel-log-cleaner?style=for-the-badge" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/jiordiviera/laravel-log-cleaner"><img src="https://img.shields.io/packagist/v/jiordiviera/laravel-log-cleaner?include_prereleases&style=for-the-badge" alt="Latest Unstable Version"></a>
<a href="https://packagist.org/packages/jiordiviera/laravel-log-cleaner"><img src="https://img.shields.io/packagist/l/jiordiviera/laravel-log-cleaner?style=for-the-badge" alt="License"></a>
<a href="https://github.com/jiordiviera/laravel-log-cleaner/actions/workflows/tests.yml"><img src="https://github.com/jiordiviera/laravel-log-cleaner/actions/workflows/tests.yml/badge.svg" alt="Tests Status" style="for-the-badge" /></a>
</p>
**Laravel Log Cleaner** is a utility package designed for the efficient management of Laravel log files. It allows developers to quickly clear log data using an Artisan command, enhancing application performance and management. This tool is compatible with Laravel versions 7, 8, 9, 10, and 11.
## Installation
You can install the package via **Composer** by executing the following command:
```bash
composer require jiordiviera/laravel-log-cleaner
```
## Compatibility
### Version 2.x (Current)
**PHP Requirements:**
- PHP 8.1+
- PHP 8.2+
- PHP 8.3+
**Laravel Support:**
- Laravel 9.x
- Laravel 10.x
- Laravel 11.x
- Laravel 12.x
### Version 1.x (Legacy)
For older PHP versions, use version 1.x:
- PHP 7.0+ to 8.2
- Laravel 7.x to 11.x
```bash
composer require jiordiviera/laravel-log-cleaner:^1.0
```
## Usage
After installation, an Artisan command is available to clear the Laravel log file with advanced options:
### Basic Usage
1. **Clear all logs:**
```bash
php artisan log:clear
```
2. **Clear logs older than specific days:**
```bash
php artisan log:clear --days=30
```
### Advanced Features (v2.0+)
#### 🔒 Safe Operations
```bash
# Preview what would be deleted (dry run)
php artisan log:clear --days=30 --dry-run
# Create backup before cleaning
php artisan log:clear --days=30 --backup
```
#### 🎯 Targeted Cleaning
```bash
# Keep only ERROR level logs
php artisan log:clear --days=0 --level=ERROR
# Clean with custom date pattern
php artisan log:clear --days=30 --pattern="/^(\d{4}-\d{2}-\d{2})/"
```
#### 📦 Compression & Memory
```bash
# Compress old logs instead of deleting
php artisan log:clear --days=30 --compress
# Force memory-efficient processing for large files
php artisan log:clear --days=30 --memory-efficient
```
#### 🚀 Combined Options
```bash
# Complete workflow with all safety features
php artisan log:clear --days=30 --backup --compress --level=ERROR --dry-run
```
### Available Options
| Option | Description | Example |
|--------|-------------|---------|
| `--days=N` | Keep logs from last N days | `--days=30` |
| `--backup` | Create backup before cleaning | `--backup` |
| `--dry-run` | Preview changes without applying | `--dry-run` |
| `--level=LEVEL` | Filter by log level (ERROR, WARNING, INFO, DEBUG) | `--level=ERROR` |
| `--pattern=REGEX` | Custom date pattern matching | `--pattern="/^(\d{4}-\d{2}-\d{2})/"` |
| `--compress` | Compress old logs instead of deleting | `--compress` |
| `--memory-efficient` | Force memory-efficient processing | `--memory-efficient` |
### Examples
- **Clear all logs:**
```bash
$ php artisan log:clear
Log file cleared successfully.
```
- **Clear logs older than 30 days:**
```bash
$ php artisan log:clear --days=30
Logs older than 30 days have been removed.
```
- **Preview changes (dry run):**
```bash
$ php artisan log:clear --days=30 --dry-run
[DRY RUN] Would remove 150 lines from laravel.log
[DRY RUN] Estimated space to free: 45.2 MB
```
- **Create backup and compress:**
```bash
$ php artisan log:clear --days=30 --backup --compress
Backup created: /path/to/laravel.log.backup.2025-07-18-14-30-15
Logs compressed to: laravel.log.old.2025-07-18-14-30-15.gz
Logs older than 30 days have been removed.
```
## Configuration
No additional configuration is necessary. The `log:clear` command is immediately available upon package installation.
## What's New in v2.0
### 🚀 Performance & Memory Optimizations
- **Memory-efficient processing** for large log files (>50MB)
- **Automatic memory threshold detection** prevents out-of-memory errors
- **Stream processing** handles multi-GB log files without memory issues
- **Regex pattern caching** improves performance on repeated operations
### 🔒 Enhanced Safety & Robustness
- **Pre-flight permission validation** prevents runtime errors
- **Backup creation** with timestamp for data recovery
- **Dry-run mode** for safe preview of operations
- **Enhanced error handling** with detailed reporting
### 🎯 Advanced Filtering
- **Log level filtering** (ERROR, WARNING, INFO, DEBUG, etc.)
- **Custom date patterns** for non-standard log formats
- **Flexible date range selection** with improved accuracy
### 📦 Archive & Compression
- **Compression support** for old logs instead of deletion
- **Automatic cleanup** of temporary files
- **Space-efficient archiving** with gzip compression
### 🔧 Breaking Changes
- **Minimum PHP version:** 8.1+ (dropped PHP 7.x support)
- **Minimum Laravel version:** 9.x+ (dropped Laravel 7.x-8.x support)
- **Enhanced command signature** with new options
### 📊 Performance Benchmarks
- Handles **1GB+ log files** without memory issues
- **50%+ performance improvement** on large file operations
- **Zero memory leaks** with proper resource management
- **Concurrent processing** support for multiple log files
## Running Tests
This package uses **Pest** for testing. You can run tests with the following command:
```bash
./vendor/bin/pest
```
Ensure your tests are organized correctly within the `tests/` directory.
## Contributing
Contributions are welcomed! Feel free to submit **Issues** or **Pull Requests** on [GitHub](https://github.com/jiordiviera/laravel-log-cleaner).
### Development Workflow
For contributors:
1. **Clone the repository:**
```bash
git clone https://github.com/jiordiviera/laravel-log-cleaner.git
```
2. **Install dependencies:**
```bash
composer install
```
3. **Run tests:**
```bash
./vendor/bin/pest
```
## About
This package was created to streamline the management of log files in Laravel applications. Instead of manually clearing the log files, you can achieve this efficiently with a single command, with the option to selectively remove older logs.
## License
The Laravel Log Cleaner is open-source software licensed under the [MIT License](https://opensource.org/licenses/MIT).
---
> **Note:** Initially developed for Laravel 11, this package remains compatible with earlier versions (7, 8, 9, 10).
For further information, visit the [GitHub repository](https://github.com/jiordiviera/laravel-log-cleaner).
@@ -0,0 +1,64 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 1.x | :white_check_mark: |
## Reporting a Vulnerability
We take the security of Laravel Log Cleaner seriously. If you discover any security-related issues, please send an email to security@jiordiviera.dev instead of using the public issue tracker.
Please include the following information in your report:
- Type of issue (e.g., buffer overflow, SQL injection, cross-site scripting, etc.)
- Full paths of source file(s) related to the manifestation of the issue
- The location of the affected source code (tag/branch/commit or direct URL)
- Any special configuration required to reproduce the issue
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit it
### What to expect
- You will receive acknowledgement of your report within 48 hours
- We will try to keep you informed about our progress throughout the process
- After the initial reply to your report, the security team will endeavor to keep you informed of the progress towards a fix and full announcement
- You may be asked for additional information or guidance
### Security Update Process
- After receiving a security report, we will verify the issue and determine its severity
- If the issue is confirmed:
- We will develop and test a fix
- We will prepare a security advisory for our users
- The fix will be applied to the latest stable version and released
- After the release, the security advisory will be published
### Public Disclosure Process
- Security vulnerabilities must be disclosed privately and will be handled on a case-by-case basis
- Public disclosure of a vulnerability will happen after a fix has been released
- The timing of the public disclosure will be coordinated between the security team and the reporter
## Security Best Practices
When using Laravel Log Cleaner, please follow these security best practices:
1. Always keep your Laravel Log Cleaner package up to date with the latest version
2. Use appropriate file permissions on your log files
3. Configure log retention periods according to your compliance requirements
4. Regularly monitor log cleaning activities
5. Implement proper access controls to log directories
6. Use environment variables for sensitive configurations
## Dependencies
Laravel Log Cleaner is built on top of Laravel and follows Laravel's security policies and best practices. We regularly monitor our dependencies for security issues and update them as needed.
## Contact
For any questions regarding this security policy, please contact jiordikengne@gmail.com.
Thank you for helping keep Laravel Log Cleaner and its users safe!
@@ -0,0 +1,63 @@
{
"name": "jiordiviera/laravel-log-cleaner",
"description": "A Laravel package to easily clean the log files.",
"keywords": [
"laravel",
"log",
"cleaner",
"logging",
"log management",
"laravel package",
"maintenance",
"log files",
"artisan"
],
"license": "MIT",
"authors": [
{
"name": "Jiordi Viera",
"email": "jiordikengne@gmail.com",
"role": "Owner",
"homepage": "https://github.com/jiordiviera"
}
],
"require": {
"php": "^8.1|^8.2|^8.3",
"illuminate/support": "^9.0|^10.0|^11.0|^12.0",
"illuminate/console": "^9.0|^10.0|^11.0|^12.0"
},
"require-dev": {
"orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
"pestphp/pest": "^1.23|^2.0|^3.0",
"pestphp/pest-plugin-laravel": "^1.4|^2.0|^3.0"
},
"autoload": {
"psr-4": {
"JiordiViera\\LaravelLogCleaner\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"JiordiViera\\LaravelLogCleaner\\Tests\\": "tests/"
}
},
"scripts": {
"test": "vendor/bin/pest",
"test-coverage": "vendor/bin/pest --coverage"
},
"extra": {
"laravel": {
"providers": [
"JiordiViera\\LaravelLogCleaner\\LaravelLogCleanerServiceProvider"
]
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
},
"sort-packages": true
}
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">./app</directory>
<directory suffix=".php">./src</directory>
</include>
</source>
</phpunit>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
<directory suffix=".php">./src</directory>
</include>
</coverage>
</phpunit>
@@ -0,0 +1,369 @@
<?php
declare(strict_types=1);
namespace JiordiViera\LaravelLogCleaner\Commands;
use Carbon\Carbon;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use InvalidArgumentException;
use SplFileObject;
use RuntimeException;
class ClearLogCommand extends Command
{
const LOG_DIRECTORY = 'logs';
const MESSAGE_NO_LOG_FILE = 'No log files found in %s';
const MESSAGE_CLEARED_ALL = 'All log files cleared successfully';
const MESSAGE_CLEARED_OLD = 'Logs older than %d days have been removed from %s';
const MESSAGE_INVALID_DAYS = 'Days must be a positive integer';
const MESSAGE_PERMISSION_ERROR = 'Permission denied for file: %s';
const MESSAGE_BACKUP_CREATED = 'Backup created: %s';
const MESSAGE_COMPRESSED = 'Logs compressed to: %s';
const MESSAGE_DRY_RUN = '[DRY RUN] Would remove %d lines from %s';
const MESSAGE_DRY_RUN_SPACE = '[DRY RUN] Estimated space to free: %s';
const MESSAGE_ZLIB_MISSING = 'The zlib extension is required for compression. Please install it to use the --compress option.';
const MEMORY_THRESHOLD = 50 * 1024 * 1024; // 50MB
const LOG_LEVELS = ['EMERGENCY', 'ALERT', 'CRITICAL', 'ERROR', 'WARNING', 'NOTICE', 'INFO', 'DEBUG'];
const DEFAULT_LOG_PATTERNS = [
'/^\[(\d{4}-\d{2}-\d{2})/',
'/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/',
'/^(\d{4}-\d{2}-\d{2})/',
];
protected $signature = 'log:clear {--days= : Number of days of logs to keep} {--backup : Create backup before cleaning} {--pattern= : Custom date pattern for logs} {--memory-efficient : Use memory-efficient processing for large files} {--compress : Compress old logs instead of deleting them} {--level= : Filter by log level (ERROR, WARNING, INFO, DEBUG)} {--dry-run : Show what would be deleted without actually deleting}';
protected $description = 'Clear the content of the log files';
private array $compiledPatterns = [];
public function handle(): int
{
try {
$logDir = $this->getLogDirectory();
$days = $this->validateDaysOption();
$this->compilePatterns();
$this->validateLogLevel();
$this->validateZlibExtension();
$logFiles = $this->getLogFiles($logDir);
if (empty($logFiles)) {
$this->warn(sprintf(self::MESSAGE_NO_LOG_FILE, $logDir));
return self::FAILURE;
}
if (!$this->option('dry-run')) {
$this->validatePermissions($logFiles);
}
foreach ($logFiles as $logFile) {
if ($this->option('backup') && !$this->option('dry-run')) {
$this->createBackup($logFile);
}
if ($days === 0 && !$this->option('level')) {
$this->clearAllLogs($logFile);
} else {
$this->clearOldLogs($logFile, $days);
}
}
return self::SUCCESS;
} catch (InvalidArgumentException $e) {
$this->error($e->getMessage());
return self::FAILURE;
} catch (Exception $e) {
$this->error('An error occurred: ' . $e->getMessage());
return self::FAILURE;
}
}
private function getLogDirectory(): string
{
return storage_path(self::LOG_DIRECTORY);
}
private function validateDaysOption(): int
{
$days = (int)$this->option('days');
if ($days < 0) {
throw new InvalidArgumentException(self::MESSAGE_INVALID_DAYS);
}
return $days;
}
private function getLogFiles(string $logDir): array
{
return array_filter(File::files($logDir), function ($file) {
return $file->getExtension() === 'log';
});
}
private function clearAllLogs($file)
{
File::put($file->getPathname(), '');
$this->info(self::MESSAGE_CLEARED_ALL);
}
private function clearOldLogs($file, int $days)
{
$cutoffDate = Carbon::now()->subDays($days)->startOfDay();
$filePath = $file->getPathname();
if ($this->shouldUseMemoryEfficientProcessing($filePath)) {
$this->clearOldLogsMemoryEfficient($filePath, $cutoffDate, $days);
} else {
$this->clearOldLogsStandard($filePath, $cutoffDate, $days);
}
if (!$this->option('dry-run')) {
$this->info(sprintf(self::MESSAGE_CLEARED_OLD, $days, $file->getFilename()));
}
}
private function filterOldLogs(array $lines, Carbon $cutoffDate): array
{
return array_filter($lines, function ($line) use ($cutoffDate) {
return $this->shouldKeepLine($line, $cutoffDate);
});
}
private function shouldKeepLine(string $line, Carbon $cutoffDate): bool
{
// First check log level filter
if (!$this->shouldKeepLineByLevel($line)) {
return false;
}
// If only filtering by level (days=0 and level specified), keep the line
if ($this->option('level') && $this->validateDaysOption() === 0) {
return true;
}
// Then check date filter
foreach ($this->compiledPatterns as $pattern => $format) {
if (preg_match($pattern, $line, $matches)) {
try {
$logDate = Carbon::createFromFormat($format, $matches[1])->startOfDay();
return $logDate->greaterThanOrEqualTo($cutoffDate);
} catch (Exception $e) {
continue;
}
}
}
return true;
}
private function compilePatterns(): void
{
$customPattern = $this->option('pattern');
if ($customPattern) {
// Validate regex pattern
if (@preg_match($customPattern, '') === false) {
throw new InvalidArgumentException('Invalid regex pattern provided: ' . preg_last_error_msg());
}
$this->compiledPatterns = [$customPattern => 'Y-m-d'];
} else {
$this->compiledPatterns = [
'/^\[(\d{4}-\d{2}-\d{2})/' => 'Y-m-d',
'/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/' => 'Y-m-d H:i:s',
'/^(\d{4}-\d{2}-\d{2})/' => 'Y-m-d',
];
}
}
private function shouldUseMemoryEfficientProcessing(string $filePath): bool
{
return $this->option('memory-efficient') || filesize($filePath) > self::MEMORY_THRESHOLD;
}
private function clearOldLogsStandard(string $filePath, Carbon $cutoffDate, int $days): void
{
$content = File::get($filePath);
$lines = explode(PHP_EOL, $content);
$newLines = $this->filterOldLogs($lines, $cutoffDate);
if ($this->option('dry-run')) {
$removedCount = count($lines) - count($newLines);
$this->info(sprintf(self::MESSAGE_DRY_RUN, $removedCount, basename($filePath)));
// Calculate estimated space to free
$linesToRemove = array_diff($lines, $newLines);
$estimatedBytes = array_sum(array_map('strlen', $linesToRemove));
$this->info(sprintf(self::MESSAGE_DRY_RUN_SPACE, $this->formatBytes($estimatedBytes)));
return;
}
if ($this->option('compress')) {
$linesToCompress = array_diff($lines, $newLines);
$this->compressOldLogs($filePath, $linesToCompress);
}
File::put($filePath, implode(PHP_EOL, $newLines));
}
private function clearOldLogsMemoryEfficient(string $filePath, Carbon $cutoffDate, int $days): void
{
if ($this->option('dry-run')) {
$this->dryRunMemoryEfficient($filePath, $cutoffDate);
return;
}
$tempFile = $filePath . '.tmp';
$compressHandle = null;
if ($this->option('compress')) {
$compressedPath = $filePath . '.old.' . date('Y-m-d-H-i-s') . '.gz';
$compressHandle = gzopen($compressedPath, 'w9');
}
$inputHandle = fopen($filePath, 'r');
$outputHandle = fopen($tempFile, 'w');
if (!$inputHandle || !$outputHandle) {
throw new RuntimeException('Unable to open file handles for processing');
}
$firstLine = true;
while (($line = fgets($inputHandle)) !== false) {
$line = rtrim($line, "\r\n");
if ($this->shouldKeepLine($line, $cutoffDate)) {
if (!$firstLine) {
fwrite($outputHandle, PHP_EOL);
}
fwrite($outputHandle, $line);
$firstLine = false;
} elseif ($compressHandle) {
gzwrite($compressHandle, $line . PHP_EOL);
}
}
fclose($inputHandle);
fclose($outputHandle);
if ($compressHandle) {
gzclose($compressHandle);
$this->info(sprintf(self::MESSAGE_COMPRESSED, basename($compressedPath)));
}
if (!rename($tempFile, $filePath)) {
unlink($tempFile);
throw new RuntimeException('Failed to replace original file with cleaned version');
}
}
private function dryRunMemoryEfficient(string $filePath, Carbon $cutoffDate): void
{
$inputHandle = fopen($filePath, 'r');
if (!$inputHandle) {
throw new RuntimeException('Unable to open file for dry run analysis');
}
$removedCount = 0;
$estimatedBytes = 0;
while (($line = fgets($inputHandle)) !== false) {
$line = rtrim($line, "\r\n");
if (!$this->shouldKeepLine($line, $cutoffDate)) {
$removedCount++;
$estimatedBytes += strlen($line);
}
}
fclose($inputHandle);
$this->info(sprintf(self::MESSAGE_DRY_RUN, $removedCount, basename($filePath)));
$this->info(sprintf(self::MESSAGE_DRY_RUN_SPACE, $this->formatBytes($estimatedBytes)));
}
private function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, 2) . ' ' . $units[$pow];
}
private function validatePermissions(array $logFiles): void
{
foreach ($logFiles as $file) {
$filePath = $file->getPathname();
if (!is_readable($filePath) || !is_writable($filePath)) {
throw new RuntimeException(sprintf(self::MESSAGE_PERMISSION_ERROR, $filePath));
}
}
}
private function createBackup($file): void
{
$filePath = $file->getPathname();
$backupPath = $filePath . '.backup.' . date('Y-m-d-H-i-s');
if (!copy($filePath, $backupPath)) {
throw new RuntimeException('Failed to create backup for: ' . $filePath);
}
$this->info(sprintf(self::MESSAGE_BACKUP_CREATED, $backupPath));
}
private function validateLogLevel(): void
{
$level = $this->option('level');
if ($level && !in_array(strtoupper($level), self::LOG_LEVELS)) {
throw new InvalidArgumentException('Invalid log level. Must be one of: ' . implode(', ', self::LOG_LEVELS));
}
}
private function validateZlibExtension(): void
{
if ($this->option('compress') && !extension_loaded('zlib')) {
throw new RuntimeException(self::MESSAGE_ZLIB_MISSING);
}
}
private function shouldKeepLineByLevel(string $line): bool
{
$filterLevel = $this->option('level');
if (!$filterLevel) {
return true;
}
// Check if line has a log level
foreach (self::LOG_LEVELS as $level) {
if (preg_match('/\.' . $level . ':/', $line)) {
return strtoupper($filterLevel) === $level;
}
}
// If no log level found in line, keep it (might be multiline log continuation)
return true;
}
private function compressOldLogs(string $filePath, array $linesToCompress): void
{
if (empty($linesToCompress)) {
return;
}
$compressedPath = $filePath . '.old.' . date('Y-m-d-H-i-s') . '.gz';
$handle = gzopen($compressedPath, 'w9');
if (!$handle) {
throw new RuntimeException('Failed to create compressed file: ' . $compressedPath);
}
foreach ($linesToCompress as $line) {
gzwrite($handle, $line . PHP_EOL);
}
gzclose($handle);
$this->info(sprintf(self::MESSAGE_COMPRESSED, $compressedPath));
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace JiordiViera\LaravelLogCleaner;
use Illuminate\Support\ServiceProvider;
use JiordiViera\LaravelLogCleaner\Commands\ClearLogCommand;
class LaravelLogCleanerServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->commands([
ClearLogCommand::class
]);
}
public function boot(): void
{
}
}
@@ -0,0 +1,506 @@
<?php
use Carbon\Carbon;
use Illuminate\Support\Facades\File;
use function Pest\Laravel\artisan;
const LOG_DIRECTORY = 'logs';
const OLD_LOG_MESSAGE = '[2023-01-01 12:00:00] test.ERROR: Old log message';
const RECENT_LOG_MESSAGE = '[%s 12:00:00] test.INFO: Recent log message';
beforeEach(function () {
$this->logDirectory = storage_path(LOG_DIRECTORY);
File::ensureDirectoryExists($this->logDirectory);
File::delete(File::files($this->logDirectory));
});
afterEach(function () {
File::delete(File::files($this->logDirectory));
});
// Test clearing all log files
it('clears all log files', function () {
// Arrange
$filePaths = [
$this->logDirectory . '/laravel.log',
$this->logDirectory . '/app.log',
];
foreach ($filePaths as $filePath) {
File::put($filePath, 'Log file content');
}
// Act
artisan('log:clear')
->assertExitCode(0);
// Assert
foreach ($filePaths as $filePath) {
expect(File::get($filePath))->toBe('');
}
});
// Test warning if no log files exist
it('warns if no log files exist', function () {
// Act
$result = artisan('log:clear');
// Assert
$result->expectsOutput('No log files found in ' . $this->logDirectory)->assertExitCode(1);
});
// Test clearing logs older than specified days across files
it('clears logs older than specified days across files', function () {
// Arrange
$recentDate = Carbon::now()->format('Y-m-d');
$recentLog = sprintf(RECENT_LOG_MESSAGE, $recentDate);
$content = OLD_LOG_MESSAGE . PHP_EOL . $recentLog;
$filePaths = [
$this->logDirectory . '/laravel.log',
$this->logDirectory . '/app.log',
];
foreach ($filePaths as $filePath) {
File::put($filePath, $content);
}
// Act
artisan('log:clear --days=30')
->assertExitCode(0);
// Assert
foreach ($filePaths as $filePath) {
expect(File::get($filePath))->not->toContain(OLD_LOG_MESSAGE)->toContain($recentLog);
}
});
// Test invalid days parameter across files
it('handles invalid days parameter across files', function () {
// Arrange
$recentDate = Carbon::now()->format('Y-m-d');
$recentLog = sprintf(RECENT_LOG_MESSAGE, $recentDate);
$content = OLD_LOG_MESSAGE . PHP_EOL . $recentLog;
$filePaths = [
$this->logDirectory . '/laravel.log',
$this->logDirectory . '/app.log',
];
foreach ($filePaths as $filePath) {
File::put($filePath, $content);
}
// Act
artisan('log:clear --days=-1')
->expectsOutput('Days must be a positive integer')
->assertExitCode(1);
});
// Test keeping logs when all are within the specified days across files
it('keeps all logs if all are within the specified days across files', function () {
// Arrange
$recentDate = Carbon::now()->subDays(5)->format('Y-m-d');
$recentLog = sprintf(RECENT_LOG_MESSAGE, $recentDate);
$filePaths = [
$this->logDirectory . '/laravel.log',
$this->logDirectory . '/app.log',
];
foreach ($filePaths as $filePath) {
File::put($filePath, $recentLog);
}
// Act
artisan('log:clear --days=10')
->assertExitCode(0);
// Assert
foreach ($filePaths as $filePath) {
expect(File::get($filePath))->toBe($recentLog);
}
});
// Test backup creation
it('creates backup when backup option is used', function () {
// Arrange
$filePath = $this->logDirectory . '/laravel.log';
$content = 'Test log content';
File::put($filePath, $content);
// Act
artisan('log:clear --backup')
->assertExitCode(0);
// Assert
$backupFiles = glob($filePath . '.backup.*');
expect($backupFiles)->toHaveCount(1);
expect(File::get($backupFiles[0]))->toBe($content);
});
// Test dry run mode
it('shows what would be removed in dry run mode', function () {
// Arrange
$recentDate = Carbon::now()->format('Y-m-d');
$recentLog = sprintf(RECENT_LOG_MESSAGE, $recentDate);
$content = OLD_LOG_MESSAGE . PHP_EOL . $recentLog;
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, $content);
// Act & Assert
artisan('log:clear --days=30 --dry-run')
->expectsOutput('[DRY RUN] Would remove 1 lines from laravel.log')
->assertExitCode(0);
// Verify file is unchanged
expect(File::get($filePath))->toBe($content);
});
// Test memory efficient processing
it('processes large files with memory efficient mode', function () {
// Arrange
$recentDate = Carbon::now()->format('Y-m-d');
$recentLog = sprintf(RECENT_LOG_MESSAGE, $recentDate);
$content = OLD_LOG_MESSAGE . PHP_EOL . $recentLog;
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, $content);
// Act
artisan('log:clear --days=30 --memory-efficient')
->assertExitCode(0);
// Assert
expect(File::get($filePath))->not->toContain(OLD_LOG_MESSAGE)->toContain($recentLog);
});
// Test custom pattern support
it('supports custom date patterns', function () {
// Arrange
$customLog = '2023-01-01 Custom log entry';
$recentLog = Carbon::now()->format('Y-m-d') . ' Recent custom log';
$content = $customLog . PHP_EOL . $recentLog;
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, $content);
// Act
artisan('log:clear', ['--days' => 30, '--pattern' => '/^(\d{4}-\d{2}-\d{2})/'])
->assertExitCode(0);
// Assert
expect(File::get($filePath))->not->toContain($customLog)->toContain($recentLog);
});
// Test log level filtering
it('filters logs by level', function () {
// Arrange
$errorLog = '[' . Carbon::now()->format('Y-m-d') . ' 12:00:00] test.ERROR: Error message';
$infoLog = '[' . Carbon::now()->format('Y-m-d') . ' 12:00:00] test.INFO: Info message';
$content = $errorLog . PHP_EOL . $infoLog;
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, $content);
// Act - keep only ERROR logs
artisan('log:clear --days=0 --level=ERROR')
->assertExitCode(0);
// Assert
expect(File::get($filePath))->toContain($errorLog)->not->toContain($infoLog);
});
// Test invalid log level
it('rejects invalid log levels', function () {
// Arrange
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, 'test content');
// Act & Assert
artisan('log:clear --level=INVALID')
->expectsOutput('Invalid log level. Must be one of: EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG')
->assertExitCode(1);
});
// Test compression creates .gz file
it('creates compressed file when compress option is used', function () {
// Arrange
$recentDate = Carbon::now()->format('Y-m-d');
$recentLog = sprintf(RECENT_LOG_MESSAGE, $recentDate);
$content = OLD_LOG_MESSAGE . PHP_EOL . $recentLog;
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, $content);
// Act
artisan('log:clear --days=30 --compress')
->assertExitCode(0);
// Assert - check .gz file exists
$gzFiles = glob($filePath . '.old.*.gz');
expect($gzFiles)->toHaveCount(1);
// Verify main file still has recent logs
expect(File::get($filePath))->toContain($recentLog)->not->toContain(OLD_LOG_MESSAGE);
});
// Test compressed file content is correct
it('compresses old logs correctly and they can be decompressed', function () {
// Arrange
$recentDate = Carbon::now()->format('Y-m-d');
$recentLog = sprintf(RECENT_LOG_MESSAGE, $recentDate);
$content = OLD_LOG_MESSAGE . PHP_EOL . $recentLog;
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, $content);
// Act
artisan('log:clear --days=30 --compress')
->assertExitCode(0);
// Assert - decompress and verify content
$gzFiles = glob($filePath . '.old.*.gz');
expect($gzFiles)->toHaveCount(1);
$decompressed = gzdecode(File::get($gzFiles[0]));
expect($decompressed)->toContain(OLD_LOG_MESSAGE);
});
// Test compress with memory efficient mode
it('compresses logs with memory efficient mode', function () {
// Arrange
$recentDate = Carbon::now()->format('Y-m-d');
$recentLog = sprintf(RECENT_LOG_MESSAGE, $recentDate);
$content = OLD_LOG_MESSAGE . PHP_EOL . $recentLog;
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, $content);
// Act
artisan('log:clear --days=30 --compress --memory-efficient')
->assertExitCode(0);
// Assert
$gzFiles = glob($filePath . '.old.*.gz');
expect($gzFiles)->toHaveCount(1);
expect(File::get($filePath))->toContain($recentLog)->not->toContain(OLD_LOG_MESSAGE);
});
// Test backup and compress combination
it('creates backup and compresses old logs', function () {
// Arrange
$recentDate = Carbon::now()->format('Y-m-d');
$recentLog = sprintf(RECENT_LOG_MESSAGE, $recentDate);
$content = OLD_LOG_MESSAGE . PHP_EOL . $recentLog;
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, $content);
// Act
artisan('log:clear --days=30 --backup --compress')
->assertExitCode(0);
// Assert - both backup and compressed file exist
$backupFiles = glob($filePath . '.backup.*');
$gzFiles = glob($filePath . '.old.*.gz');
expect($backupFiles)->toHaveCount(1);
expect($gzFiles)->toHaveCount(1);
// Backup should contain original content
expect(File::get($backupFiles[0]))->toBe($content);
});
// Test level filtering with days combination
it('filters by level and days together', function () {
// Arrange
$recentDate = Carbon::now()->format('Y-m-d');
$oldDate = Carbon::now()->subDays(60)->format('Y-m-d');
$errorLog = '[' . $recentDate . ' 12:00:00] test.ERROR: Recent error';
$oldErrorLog = '[' . $oldDate . ' 12:00:00] test.ERROR: Old error';
$infoLog = '[' . $recentDate . ' 12:00:00] test.INFO: Recent info';
$content = $oldErrorLog . PHP_EOL . $errorLog . PHP_EOL . $infoLog;
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, $content);
// Act - keep only ERROR logs from last 30 days
artisan('log:clear --days=30 --level=ERROR')
->assertExitCode(0);
// Assert
$result = File::get($filePath);
expect($result)->toContain($errorLog)
->not->toContain($oldErrorLog)
->not->toContain($infoLog);
});
// Test dry run shows correct space estimation
it('shows space estimation in dry run mode', function () {
// Arrange
$recentDate = Carbon::now()->format('Y-m-d');
$recentLog = sprintf(RECENT_LOG_MESSAGE, $recentDate);
$content = str_repeat(OLD_LOG_MESSAGE . PHP_EOL, 100) . $recentLog;
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, $content);
// Act - run dry-run and capture output
artisan('log:clear --days=30 --dry-run')
->expectsOutputToContain('Would remove')
->expectsOutputToContain('Estimated')
->assertExitCode(0);
// Verify file unchanged - this is the key behavior of dry-run
expect(File::get($filePath))->toBe($content);
});
// Test custom pattern with invalid regex
it('handles invalid regex pattern gracefully', function () {
// Arrange
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, 'test content');
// Act & Assert - invalid regex pattern should fail with error
artisan('log:clear --days=30 --pattern="[invalid"')
->expectsOutputToContain('Invalid regex pattern')
->assertExitCode(1);
});
// Test all options combined
it('handles all options together correctly', function () {
// Arrange
$recentDate = Carbon::now()->format('Y-m-d');
$recentLog = '[' . $recentDate . ' 12:00:00] test.ERROR: Recent error';
$content = OLD_LOG_MESSAGE . PHP_EOL . $recentLog;
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, $content);
// Act - dry run with all options (should not modify anything)
artisan('log:clear --days=30 --backup --compress --level=ERROR --memory-efficient --dry-run')
->assertExitCode(0);
// Assert - file unchanged in dry-run
expect(File::get($filePath))->toBe($content);
// No backup or compressed files created
$backupFiles = glob($filePath . '.backup.*');
$gzFiles = glob($filePath . '.old.*.gz');
expect($backupFiles)->toBeEmpty();
expect($gzFiles)->toBeEmpty();
});
// Test empty log file
it('handles empty log file gracefully', function () {
// Arrange
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, '');
// Act
artisan('log:clear --days=30')
->assertExitCode(0);
// Assert - file still empty
expect(File::get($filePath))->toBe('');
});
// Test file with only whitespace
it('handles file with only whitespace', function () {
// Arrange
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, " \n \n ");
// Act
artisan('log:clear --days=30')
->assertExitCode(0);
// Assert - whitespace preserved or cleaned
expect(File::exists($filePath))->toBeTrue();
});
// Test multiple backup creations don't conflict
it('creates multiple backups without conflicts', function () {
// Arrange
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, 'test content');
// Act - create first backup
artisan('log:clear --backup')
->assertExitCode(0);
// Restore content and create second backup (in same second if possible)
File::put($filePath, 'test content 2');
artisan('log:clear --backup')
->assertExitCode(0);
// Assert - should have 2 backup files
$backupFiles = glob($filePath . '.backup.*');
expect(count($backupFiles))->toBeGreaterThanOrEqual(1);
});
// Test pattern validation
it('validates regex patterns properly', function () {
// Arrange
$filePath = $this->logDirectory . '/laravel.log';
$oldDate = Carbon::now()->subDays(60)->format('Y-m-d');
$recentDate = Carbon::now()->format('Y-m-d');
File::put($filePath, "[{$oldDate}] old log\n[{$recentDate}] recent log");
// Act - valid pattern should work
artisan('log:clear', ['--days' => 30, '--pattern' => '/^\[(\d{4}-\d{2}-\d{2})\]/'])
->assertExitCode(0);
// Verify recent log kept, old removed
$result = File::get($filePath);
expect($result)->toContain($recentDate)->not->toContain($oldDate);
});
// Test large file triggers memory efficient automatically
it('automatically uses memory efficient mode for large files', function () {
// Arrange
$filePath = $this->logDirectory . '/laravel.log';
// Create content larger than 50MB threshold
$largeContent = str_repeat(OLD_LOG_MESSAGE . PHP_EOL, 10000);
File::put($filePath, $largeContent);
$fileSize = filesize($filePath);
// Act - should auto-enable memory efficient if > 50MB
artisan('log:clear --days=30')
->assertExitCode(0);
// Assert - command should complete without memory errors
expect(File::exists($filePath))->toBeTrue();
});
// Test compress without old logs
it('handles compress option when no old logs to compress', function () {
// Arrange
$recentDate = Carbon::now()->format('Y-m-d');
$recentLog = sprintf(RECENT_LOG_MESSAGE, $recentDate);
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, $recentLog);
// Act
artisan('log:clear --days=30 --compress')
->assertExitCode(0);
// Assert - no .gz file created
$gzFiles = glob($filePath . '.old.*.gz');
expect($gzFiles)->toBeEmpty();
// Recent log still intact
expect(File::get($filePath))->toBe($recentLog);
});
// Test level filter with multiline stack traces
it('preserves stack traces when filtering by level', function () {
// Arrange
$recentDate = Carbon::now()->format('Y-m-d');
$errorWithTrace = <<<LOG
[{$recentDate} 12:00:00] test.ERROR: Error message
#0 /path/to/file.php(10): function()
#1 /path/to/another.php(20): anotherFunction()
LOG;
$infoLog = '[' . $recentDate . ' 12:00:00] test.INFO: Info message';
$content = $errorWithTrace . PHP_EOL . $infoLog;
$filePath = $this->logDirectory . '/laravel.log';
File::put($filePath, $content);
// Act - keep only ERROR
artisan('log:clear --days=0 --level=ERROR')
->assertExitCode(0);
// Assert - ERROR with stack trace kept, INFO removed
$result = File::get($filePath);
expect($result)->toContain('ERROR: Error message')
->toContain('#0 /path/to/file.php')
->not->toContain('INFO: Info message');
});
@@ -0,0 +1,3 @@
<?php
uses(\JiordiViera\LaravelLogCleaner\Tests\TestCase::class)->in(__DIR__);
@@ -0,0 +1,15 @@
<?php
namespace JiordiViera\LaravelLogCleaner\Tests;
use JiordiViera\LaravelLogCleaner\LaravelLogCleanerServiceProvider;
use \Orchestra\Testbench\TestCase as OrchestraTestCase;
class TestCase extends OrchestraTestCase
{
protected function getPackageProviders($app): array
{
return [LaravelLogCleanerServiceProvider::class];
}
}