-
-
Notifications
You must be signed in to change notification settings - Fork 1
Custom Storage
Jean-Marc Strauven edited this page Aug 6, 2025
·
2 revisions
Complete guide for configuring custom storage backends for Laravel ChronoTrace, including AWS S3, MinIO, and custom solutions.
Create S3 Bucket:
# Using AWS CLI
aws s3 mb s3://my-chronotrace-bucket --region us-east-1
# Enable versioning
aws s3api put-bucket-versioning \
--bucket my-chronotrace-bucket \
--versioning-configuration Status=Enabled
# Configure lifecycle policy for cost optimization
cat > lifecycle-policy.json << EOF
{
"Rules": [
{
"ID": "ChronoTraceLifecycle",
"Status": "Enabled",
"Filter": {"Prefix": "traces/"},
"Transitions": [
{
"Days": 30,
"StorageClass": "STANDARD_IA"
},
{
"Days": 90,
"StorageClass": "GLACIER"
}
],
"Expiration": {
"Days": 365
}
}
]
}
EOF
aws s3api put-bucket-lifecycle-configuration \
--bucket my-chronotrace-bucket \
--lifecycle-configuration file://lifecycle-policy.jsonCreate IAM User and Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ChronoTraceS3Access",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket",
"s3:GetBucketLocation"
],
"Resource": [
"arn:aws:s3:::my-chronotrace-bucket",
"arn:aws:s3:::my-chronotrace-bucket/*"
]
}
]
}// config/chronotrace.php
'storage' => 's3',
's3' => [
'bucket' => env('CHRONOTRACE_S3_BUCKET', 'my-chronotrace-bucket'),
'region' => env('CHRONOTRACE_S3_REGION', 'us-east-1'),
'path_prefix' => env('CHRONOTRACE_S3_PREFIX', 'traces'),
'storage_class' => 'STANDARD_IA',
'server_side_encryption' => 'AES256',
],Environment Variables:
# .env
CHRONOTRACE_STORAGE=s3
CHRONOTRACE_S3_BUCKET=my-chronotrace-bucket
CHRONOTRACE_S3_REGION=us-east-1
CHRONOTRACE_S3_PREFIX=production/traces
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...# Download and install MinIO
wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
sudo mv minio /usr/local/bin/
# Create MinIO user and directories
sudo useradd -r minio-user -s /sbin/nologin
sudo mkdir -p /opt/minio/data
sudo chown minio-user:minio-user /opt/minio/data# Create systemd service
sudo tee /etc/systemd/system/minio.service << EOF
[Unit]
Description=MinIO
Documentation=https://docs.min.io
Wants=network-online.target
After=network-online.target
[Service]
WorkingDirectory=/opt/minio
User=minio-user
Group=minio-user
EnvironmentFile=/etc/default/minio
ExecStart=/usr/local/bin/minio server \\$MINIO_OPTS \\$MINIO_VOLUMES
Restart=always
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
# Configure MinIO
sudo tee /etc/default/minio << EOF
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin123
MINIO_VOLUMES="/opt/minio/data"
MINIO_OPTS="--console-address :9090"
EOF
# Start MinIO
sudo systemctl enable minio
sudo systemctl start minio# Install MinIO client
wget https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc
sudo mv mc /usr/local/bin/
# Configure alias
mc alias set myminio http://localhost:9000 minioadmin minioadmin123
# Create bucket
mc mb myminio/chronotrace
# Set policy
mc policy set download myminio/chronotrace# .env
CHRONOTRACE_STORAGE=s3
CHRONOTRACE_S3_BUCKET=chronotrace
CHRONOTRACE_S3_ENDPOINT=http://localhost:9000
CHRONOTRACE_S3_USE_PATH_STYLE_ENDPOINT=true
AWS_ACCESS_KEY_ID=minioadmin
AWS_SECRET_ACCESS_KEY=minioadmin123php artisan make:migration create_chronotrace_traces_table// Migration file
public function up()
{
Schema::create('chronotrace_traces', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('method', 10);
$table->string('url', 2048);
$table->string('route')->nullable();
$table->integer('status');
$table->integer('duration_ms');
$table->integer('memory_mb');
$table->integer('user_id')->nullable();
$table->json('request_data')->nullable();
$table->json('response_data')->nullable();
$table->json('events');
$table->json('metadata')->nullable();
$table->timestamp('created_at');
$table->index(['status', 'created_at']);
$table->index(['route', 'created_at']);
$table->index(['user_id', 'created_at']);
$table->index(['duration_ms', 'created_at']);
});
}// app/Storage/DatabaseStorageDriver.php
use Grazulex\LaravelChronotrace\Contracts\StorageDriver;
class DatabaseStorageDriver implements StorageDriver
{
public function store(string $traceId, array $data): bool
{
try {
DB::table('chronotrace_traces')->insert([
'id' => $traceId,
'method' => $data['request']['method'],
'url' => $data['request']['url'],
'route' => $data['request']['route'] ?? null,
'status' => $data['response']['status'],
'duration_ms' => $data['performance']['total_duration_ms'],
'memory_mb' => round($data['performance']['memory_peak_mb'], 2),
'user_id' => $data['request']['user_id'] ?? null,
'request_data' => json_encode($data['request']),
'response_data' => json_encode($data['response']),
'events' => json_encode($data['events']),
'metadata' => json_encode($data['metadata'] ?? []),
'created_at' => now(),
]);
return true;
} catch (Exception $e) {
Log::error('Failed to store trace in database', [
'trace_id' => $traceId,
'error' => $e->getMessage()
]);
return false;
}
}
public function retrieve(string $traceId): ?array
{
$record = DB::table('chronotrace_traces')->where('id', $traceId)->first();
if (!$record) {
return null;
}
return [
'trace_id' => $record->id,
'request' => json_decode($record->request_data, true),
'response' => json_decode($record->response_data, true),
'events' => json_decode($record->events, true),
'metadata' => json_decode($record->metadata, true),
];
}
public function delete(string $traceId): bool
{
return DB::table('chronotrace_traces')->where('id', $traceId)->delete() > 0;
}
public function exists(string $traceId): bool
{
return DB::table('chronotrace_traces')->where('id', $traceId)->exists();
}
public function list(array $filters = []): array
{
$query = DB::table('chronotrace_traces');
if (isset($filters['status'])) {
$query->where('status', $filters['status']);
}
if (isset($filters['route'])) {
$query->where('route', 'like', $filters['route']);
}
if (isset($filters['since'])) {
$query->where('created_at', '>=', $filters['since']);
}
return $query->orderBy('created_at', 'desc')
->limit($filters['limit'] ?? 100)
->get()
->map(fn($record) => [
'id' => $record->id,
'method' => $record->method,
'route' => $record->route,
'status' => $record->status,
'duration' => $record->duration_ms,
'created_at' => $record->created_at,
])
->toArray();
}
public function purge(int $olderThanDays): int
{
return DB::table('chronotrace_traces')
->where('created_at', '<', now()->subDays($olderThanDays))
->delete();
}
}// config/chronotrace.php
'storage' => 'database',
// app/Providers/AppServiceProvider.php
public function boot()
{
$this->app->afterResolving(StorageManager::class, function (StorageManager $manager) {
$manager->extend('database', function ($app, $config) {
return new DatabaseStorageDriver();
});
});
}// app/Storage/RedisStorageDriver.php
use Grazulex\LaravelChronotrace\Contracts\StorageDriver;
class RedisStorageDriver implements StorageDriver
{
private $redis;
private $keyPrefix;
private $ttl;
public function __construct(array $config = [])
{
$this->redis = Redis::connection($config['connection'] ?? 'default');
$this->keyPrefix = $config['key_prefix'] ?? 'chronotrace:traces:';
$this->ttl = $config['ttl'] ?? 86400 * 15; // 15 days
}
public function store(string $traceId, array $data): bool
{
try {
$key = $this->keyPrefix . $traceId;
$compressed = gzcompress(json_encode($data), 6);
return $this->redis->setex($key, $this->ttl, $compressed);
} catch (Exception $e) {
Log::error('Failed to store trace in Redis', [
'trace_id' => $traceId,
'error' => $e->getMessage()
]);
return false;
}
}
public function retrieve(string $traceId): ?array
{
try {
$key = $this->keyPrefix . $traceId;
$compressed = $this->redis->get($key);
if (!$compressed) {
return null;
}
$data = gzuncompress($compressed);
return json_decode($data, true);
} catch (Exception $e) {
Log::error('Failed to retrieve trace from Redis', [
'trace_id' => $traceId,
'error' => $e->getMessage()
]);
return null;
}
}
public function delete(string $traceId): bool
{
$key = $this->keyPrefix . $traceId;
return $this->redis->del($key) > 0;
}
public function exists(string $traceId): bool
{
$key = $this->keyPrefix . $traceId;
return $this->redis->exists($key) > 0;
}
public function list(array $filters = []): array
{
$pattern = $this->keyPrefix . '*';
$keys = $this->redis->keys($pattern);
$traces = [];
foreach (array_slice($keys, 0, $filters['limit'] ?? 100) as $key) {
$traceId = str_replace($this->keyPrefix, '', $key);
$ttl = $this->redis->ttl($key);
$traces[] = [
'id' => $traceId,
'expires_in' => $ttl,
];
}
return $traces;
}
public function purge(int $olderThanDays): int
{
// Redis handles TTL automatically, so we just return 0
return 0;
}
}// config/chronotrace.php
'storage' => 'redis',
'redis' => [
'connection' => 'cache',
'key_prefix' => 'chronotrace:traces:',
'ttl' => 86400 * 15, // 15 days
],// app/Storage/ElasticsearchStorageDriver.php
use Elasticsearch\ClientBuilder;
use Grazulex\LaravelChronotrace\Contracts\StorageDriver;
class ElasticsearchStorageDriver implements StorageDriver
{
private $client;
private $index;
public function __construct(array $config = [])
{
$this->client = ClientBuilder::create()
->setHosts($config['hosts'] ?? ['localhost:9200'])
->build();
$this->index = $config['index'] ?? 'chronotrace-traces';
$this->createIndexIfNotExists();
}
private function createIndexIfNotExists()
{
$params = [
'index' => $this->index,
'body' => [
'mappings' => [
'properties' => [
'trace_id' => ['type' => 'keyword'],
'method' => ['type' => 'keyword'],
'route' => ['type' => 'keyword'],
'status' => ['type' => 'integer'],
'duration' => ['type' => 'integer'],
'timestamp' => ['type' => 'date'],
'events' => ['type' => 'nested'],
'request' => ['type' => 'object'],
'response' => ['type' => 'object'],
]
]
]
];
if (!$this->client->indices()->exists(['index' => $this->index])) {
$this->client->indices()->create($params);
}
}
public function store(string $traceId, array $data): bool
{
try {
$params = [
'index' => $this->index,
'id' => $traceId,
'body' => array_merge($data, [
'trace_id' => $traceId,
'timestamp' => now()->toISOString(),
])
];
$response = $this->client->index($params);
return isset($response['result']) && $response['result'] === 'created';
} catch (Exception $e) {
Log::error('Failed to store trace in Elasticsearch', [
'trace_id' => $traceId,
'error' => $e->getMessage()
]);
return false;
}
}
public function retrieve(string $traceId): ?array
{
try {
$params = [
'index' => $this->index,
'id' => $traceId
];
$response = $this->client->get($params);
return $response['_source'] ?? null;
} catch (Exception $e) {
return null;
}
}
public function delete(string $traceId): bool
{
try {
$params = [
'index' => $this->index,
'id' => $traceId
];
$response = $this->client->delete($params);
return isset($response['result']) && $response['result'] === 'deleted';
} catch (Exception $e) {
return false;
}
}
public function exists(string $traceId): bool
{
try {
$params = [
'index' => $this->index,
'id' => $traceId
];
return $this->client->exists($params);
} catch (Exception $e) {
return false;
}
}
public function list(array $filters = []): array
{
$params = [
'index' => $this->index,
'body' => [
'query' => [
'bool' => [
'must' => []
]
],
'sort' => [
'timestamp' => ['order' => 'desc']
],
'size' => $filters['limit'] ?? 100
]
];
// Add filters
if (isset($filters['status'])) {
$params['body']['query']['bool']['must'][] = [
'term' => ['status' => $filters['status']]
];
}
if (isset($filters['route'])) {
$params['body']['query']['bool']['must'][] = [
'wildcard' => ['route' => $filters['route']]
];
}
try {
$response = $this->client->search($params);
return array_map(function($hit) {
return $hit['_source'];
}, $response['hits']['hits']);
} catch (Exception $e) {
return [];
}
}
public function purge(int $olderThanDays): int
{
$params = [
'index' => $this->index,
'body' => [
'query' => [
'range' => [
'timestamp' => [
'lt' => now()->subDays($olderThanDays)->toISOString()
]
]
]
]
];
try {
$response = $this->client->deleteByQuery($params);
return $response['deleted'] ?? 0;
} catch (Exception $e) {
return 0;
}
}
}// app/Storage/MultiBackendStorageDriver.php
use Grazulex\LaravelChronotrace\Contracts\StorageDriver;
class MultiBackendStorageDriver implements StorageDriver
{
private $primary;
private $backup;
public function __construct(array $config = [])
{
$this->primary = app($config['primary']);
$this->backup = app($config['backup']);
}
public function store(string $traceId, array $data): bool
{
$primaryResult = $this->primary->store($traceId, $data);
// Always try backup storage
$backupResult = $this->backup->store($traceId, $data);
return $primaryResult || $backupResult;
}
public function retrieve(string $traceId): ?array
{
// Try primary first
$data = $this->primary->retrieve($traceId);
if ($data === null) {
// Fallback to backup
$data = $this->backup->retrieve($traceId);
}
return $data;
}
public function delete(string $traceId): bool
{
$primaryDeleted = $this->primary->delete($traceId);
$backupDeleted = $this->backup->delete($traceId);
return $primaryDeleted || $backupDeleted;
}
public function exists(string $traceId): bool
{
return $this->primary->exists($traceId) ||
$this->backup->exists($traceId);
}
public function list(array $filters = []): array
{
// Merge results from both backends
$primaryTraces = $this->primary->list($filters);
$backupTraces = $this->backup->list($filters);
$allTraces = array_merge($primaryTraces, $backupTraces);
// Remove duplicates and sort
$uniqueTraces = collect($allTraces)
->unique('id')
->sortByDesc('created_at')
->take($filters['limit'] ?? 100)
->values()
->toArray();
return $uniqueTraces;
}
public function purge(int $olderThanDays): int
{
$primaryPurged = $this->primary->purge($olderThanDays);
$backupPurged = $this->backup->purge($olderThanDays);
return max($primaryPurged, $backupPurged);
}
}// config/chronotrace.php
'storage' => 'multi',
'multi' => [
'primary' => 'redis',
'backup' => 's3',
],// tests/Unit/Storage/CustomStorageDriverTest.php
class CustomStorageDriverTest extends TestCase
{
private $driver;
protected function setUp(): void
{
parent::setUp();
$this->driver = new CustomStorageDriver($config);
}
public function test_can_store_and_retrieve_trace()
{
$traceId = 'test_trace_' . uniqid();
$data = [
'request' => ['method' => 'GET', 'url' => '/test'],
'response' => ['status' => 200],
'events' => [],
];
// Store trace
$stored = $this->driver->store($traceId, $data);
$this->assertTrue($stored);
// Verify exists
$this->assertTrue($this->driver->exists($traceId));
// Retrieve trace
$retrieved = $this->driver->retrieve($traceId);
$this->assertNotNull($retrieved);
$this->assertEquals($data['request']['method'], $retrieved['request']['method']);
// Clean up
$this->driver->delete($traceId);
}
public function test_can_list_traces()
{
// Store multiple traces
for ($i = 0; $i < 5; $i++) {
$traceId = 'test_trace_' . $i;
$this->driver->store($traceId, $this->generateTraceData());
}
// List traces
$traces = $this->driver->list(['limit' => 10]);
$this->assertGreaterThanOrEqual(5, count($traces));
// Clean up
for ($i = 0; $i < 5; $i++) {
$this->driver->delete('test_trace_' . $i);
}
}
public function test_can_purge_old_traces()
{
// This test depends on your storage implementation
$purged = $this->driver->purge(30);
$this->assertIsInt($purged);
}
}// tests/Performance/StoragePerformanceTest.php
class StoragePerformanceTest extends TestCase
{
public function test_storage_performance_benchmark()
{
$driver = app(StorageDriver::class);
$traceData = $this->generateLargeTraceData();
// Test write performance
$writeStart = microtime(true);
for ($i = 0; $i < 100; $i++) {
$traceId = 'perf_test_' . $i;
$driver->store($traceId, $traceData);
}
$writeTime = microtime(true) - $writeStart;
// Test read performance
$readStart = microtime(true);
for ($i = 0; $i < 100; $i++) {
$traceId = 'perf_test_' . $i;
$driver->retrieve($traceId);
}
$readTime = microtime(true) - $readStart;
// Assert performance requirements
$this->assertLessThan(10.0, $writeTime, 'Write performance too slow');
$this->assertLessThan(5.0, $readTime, 'Read performance too slow');
// Clean up
for ($i = 0; $i < 100; $i++) {
$driver->delete('perf_test_' . $i);
}
}
}// app/Services/StorageHealthMonitor.php
class StorageHealthMonitor
{
public function checkHealth(): array
{
$driver = app(StorageDriver::class);
return [
'connectivity' => $this->testConnectivity($driver),
'write_performance' => $this->testWritePerformance($driver),
'read_performance' => $this->testReadPerformance($driver),
'storage_usage' => $this->getStorageUsage($driver),
];
}
private function testConnectivity(StorageDriver $driver): bool
{
try {
$testId = 'health_check_' . time();
$testData = ['test' => true];
$stored = $driver->store($testId, $testData);
$exists = $driver->exists($testId);
$retrieved = $driver->retrieve($testId);
$deleted = $driver->delete($testId);
return $stored && $exists && $retrieved && $deleted;
} catch (Exception $e) {
return false;
}
}
}# Create maintenance command
php artisan make:command ChronoTrace:Maintenance// app/Console/Commands/ChronoTraceMaintenance.php
class ChronoTraceMaintenance extends Command
{
protected $signature = 'chronotrace:maintenance {--dry-run}';
public function handle()
{
$this->info('Starting ChronoTrace maintenance...');
// Check storage health
$this->checkStorageHealth();
// Optimize storage
$this->optimizeStorage();
// Clean up old traces
$this->cleanupOldTraces();
$this->info('Maintenance completed!');
}
private function checkStorageHealth()
{
$monitor = app(StorageHealthMonitor::class);
$health = $monitor->checkHealth();
if (!$health['connectivity']) {
$this->error('Storage connectivity issue detected!');
}
$this->info("Write performance: {$health['write_performance']}ms");
$this->info("Read performance: {$health['read_performance']}ms");
}
}- S3 & MinIO Storage - Detailed cloud storage setup
- Configuration - Storage configuration options
- Production Monitoring - Storage monitoring in production
- API Reference - Storage driver API reference
Build the perfect storage solution for your needs! Choose from multiple backends or create custom drivers to match your infrastructure requirements.
- Getting Started - Install and configure ChronoTrace
- Examples - Real-world debugging scenarios
- Production Guide - Deploy safely in production
- Troubleshooting - Solve common issues
| Section | Page | Description |
|---|---|---|
| π Basics | Your First Trace | Step-by-step beginner guide |
| ποΈ Config | Recording Modes | Choose when to record traces |
| π‘οΈ Security | Security & PII | Protect sensitive data |
| π§ Tools | Commands | Complete command reference |
- π¬ GitHub Discussions - Community support
- π Report Issues - Bug reports & feature requests
- π§ Contact - Direct support
- π‘ Feature Requests - Suggest improvements
ChronoTrace helps Laravel developers debug applications faster with intelligent request tracing.
Made with β€οΈ by Grazulex β’ Documentation updated August 2024