Log, Null & Custom Sinks
The log sink#
Writes events to a Laravel log channel — useful in development, as a local audit trail, and as the built-in fallback destination when other sinks exhaust their retries.
SIEM_SINKS=log
SIEM_LOG_CHANNEL=security # optional; defaults to your default channel
| Key | Env | Default |
|---|---|---|
sinks.log.channel |
SIEM_LOG_CHANNEL |
default channel |
sinks.log.level |
— | info |
Pair it with a dedicated channel in config/logging.php for a tidy on-box audit file:
'security' => [
'driver' => 'daily',
'path' => storage_path('logs/security.log'),
'days' => 30,
],
The null sink#
Discards everything. Useful as a kill switch in tests or while wiring things up:
SIEM_SINKS=null
Writing a custom sink#
Implement the Sink contract — one method that receives the ECS document and throws on failure (so the queue can retry):
namespace App\Siem;
use Inqaba\Gatekeeper\Sinks\Sink;
class KafkaSink implements Sink
{
public function __construct(private KafkaProducer $producer) {}
public function send(array $event): void
{
$this->producer->produce('security-events', json_encode($event));
}
public function name(): string
{
return 'kafka';
}
}
Register it in a service provider:
use Inqaba\Gatekeeper\Facades\Siem;
public function boot(): void
{
Siem::extend('kafka', fn ($app) => new KafkaSink($app->make(KafkaProducer::class)));
}
Activate it like any built-in sink:
SIEM_SINKS=sentinel,kafka
Your sink automatically participates in the whole pipeline: queue shipping with retries, GeoIP enrichment, redaction, the global severity floor, and — if you add a minimum_severity key under sinks.kafka in the config — per-sink severity filtering.
Contract notes#
send()receives the final ECS document as an array (post-redaction, post-enrichment). Ship it as-is where possible so downstream consumers get consistent fields.- Throw on failure. Exceptions trigger the retry/backoff cycle; after the final attempt the event is written to the local log with the full payload. Swallowing errors silently loses events.
- Sinks are resolved once and cached per worker process — keep constructors cheap and connection setup lazy if your transport is heavy.
Ideas#
- Kafka / Kinesis / PubSub — stream into a data lake or detection pipeline
- Elasticsearch / OpenSearch — the events are already ECS; index them directly
- PagerDuty / Opsgenie — a
minimum_severity: criticalincident sink - Database table — a quick local security dashboard without SIEM round-trips