Skip to main content

Audit Logging

Production-ready audit logging for Cedar authorization decisions with structured JSON output.

Introduction

Production-ready audit logging for Cedar authorization decisions with structured JSON output.

Installation

Add the audit logging module to your build.sbt:

libraryDependencies ++= Seq(
// Core audit logging
"io.github.devnico" %% "cedar4s-observability-audit" % "0.0.0-SNAPSHOT",

// For SLF4J with Logback (recommended for production)
"ch.qos.logback" % "logback-classic" % "1.5.15",
"net.logstash.logback" % "logstash-logback-encoder" % "8.0"
)

Minimal Setup (No External Dependencies)

For simple use cases without external logging frameworks:

libraryDependencies += "io.github.devnico" %% "cedar4s-observability-audit" % "0.0.0-SNAPSHOT"

Quick Start

Basic Usage

import cedar4s.observability.audit._
import scala.concurrent.ExecutionContext.Implicits.global
import cedar4s.capability.instances._

// Create an SLF4J audit logger (recommended)
val auditLogger = Slf4jAuditLogger[Future]()

// Create an audit interceptor
val interceptor = AuditInterceptor(auditLogger)

// Integrate with Cedar runtime
val runtime = CedarRuntime(engine, store, resolver)
.withInterceptor(interceptor)

// All authorization checks are now automatically logged
val session = runtime.session(user)
session.check(action, resource).require

That's it! Every authorization decision will now be logged with full context.

Logger Implementations

LoggerUse CaseKey Features
Slf4jAuditLoggerProductionAsync, rotation, SLF4J integration
JsonAuditLoggerDevelopmentNo dependencies, direct output
FileAuditLoggerSimple deploymentsBuilt-in rotation
NoOpAuditLoggerTestingZero overhead

Configuration

Production Logback Configuration

Create src/main/resources/logback.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<appender name="ASYNC_AUDIT" class="ch.qos.logback.classic.AsyncAppender">
<neverBlock>true</neverBlock>
<queueSize>1024</queueSize>
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="AUDIT_FILE"/>
</appender>

<appender name="AUDIT_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/cedar/audit.json</file>

<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>/var/log/cedar/audit-%d{yyyy-MM-dd}.%i.json.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>90</maxHistory>
<totalSizeCap>50GB</totalSizeCap>
</rollingPolicy>

<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<prettyPrint>false</prettyPrint>
<timestampPattern>yyyy-MM-dd'T'HH:mm:ss.SSSX</timestampPattern>
<customFields>{"service":"cedar-authz","environment":"production"}</customFields>
</encoder>
</appender>

<logger name="cedar4s.audit" level="INFO" additivity="false">
<appender-ref ref="ASYNC_AUDIT"/>
</logger>

<root level="WARN">
<appender-ref ref="CONSOLE"/>
</root>

</configuration>

Development Logback Configuration

For development, use simpler configuration with console output:

<configuration>
<appender name="AUDIT_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<prettyPrint>true</prettyPrint>
</encoder>
</appender>

<logger name="cedar4s.audit" level="DEBUG" additivity="false">
<appender-ref ref="AUDIT_CONSOLE"/>
</logger>

<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

Event Format

The logstash-logback-encoder produces structured JSON events:

{
"@timestamp": "2026-01-30T10:15:32.456Z",
"timestamp": "2026-01-30T10:15:32.456Z",
"principal": {
"entityType": "User",
"entityId": "alice",
"cedarUid": "User::\"alice\""
},
"action": "Document::View",
"resource": {
"entityType": "Document",
"entityId": "doc-123",
"cedarUid": "Document::\"doc-123\""
},
"decision": {
"allow": true,
"policies": ["policy-allow-view"],
"policiesSatisfied": ["policy-allow-view"],
"policiesDenied": []
},
"allowed": true,
"denied": false,
"durationMs": 1.5,
"durationNanos": 1500000,
"sessionId": "session-abc-123",
"requestId": "req-xyz-789",
"context": {
"clientIp": "192.168.1.1",
"userAgent": "Mozilla/5.0"
},
"service": "cedar-authz",
"environment": "production"
}

Integration Patterns

HTTP Frameworks

Extract HTTP context into audit events:

import cedar4s.observability.audit._
import org.http4s._
import cats.effect.IO

def createAuditInterceptor(
auditLogger: AuditLogger[IO]
): Request[IO] => AuditInterceptor[IO] = { request =>

AuditInterceptor.withExtractors[IO](
logger = auditLogger,
sessionIdExtractor = _ => {
request.headers.get[Cookie]
.flatMap(_.values.toList.find(_.name == "sessionId"))
.map(_.content)
},
requestIdExtractor = _ => request.headers.get[`X-Request-ID`].map(_.id),
contextExtractor = _ => Map(
"clientIp" -> request.remoteAddr.getOrElse("unknown"),
"userAgent" -> request.headers.get[`User-Agent`].map(_.value).getOrElse("unknown")
)
)
}

Multi-Tenant Applications

import cedar4s.observability.audit._
import scala.concurrent.Future

case class TenantContext(tenantId: String, tier: String)

def createTenantAuditInterceptor(
auditLogger: AuditLogger[Future],
getTenantContext: () => TenantContext
): AuditInterceptor[Future] = {

AuditInterceptor.withExtractors[Future](
logger = auditLogger,
sessionIdExtractor = _ => Some(s"tenant-${getTenantContext().tenantId}"),
contextExtractor = _ => {
val tenant = getTenantContext()
Map("tenantId" -> tenant.tenantId, "tier" -> tenant.tier)
}
)
}

Production Considerations

  • Use Async Appenders: Configure async appenders with appropriate queue sizes and set neverBlock=true
  • Configure Retention Policies: Common periods: SOC 2 (90 days), HIPAA (6 years), PCI-DSS (1 year)
  • Monitor Log Volume: Track audit log volume and set up alerts for dropped events and disk space
  • Secure Log Files: Use restrictive permissions, encryption at rest, and SELinux/AppArmor policies
  • Forward to SIEM: Integrate with centralized logging systems like Splunk, Datadog, or rsyslog
  • Add Service Metadata: Include deployment information in all events using custom fields

For detailed production deployment guides, see: