S3 Logs
Read mock nginx access logs from S3 through the AWS provider, parse each line with parse string patterns, and expose the result through rank serve.
Source
examples/s3-logs/main.rank
use aws::{ S3 }
use std::Env
use std::HTTP
use std::Runtime
use std::collections::{ count, filter, flatMap, length, map }
use std::string::{ isBlank, startsWith, split, trim }
LogDay = `2026-05-11` | `2026-05-12`
StatusClass = `2xx` | `3xx` | `4xx` | `5xx` | `other`
AccessLogFile = Object {
format: `nginx-access`,
server: string,
day: LogDay,
raw: string,
}
AccessLogEntry = Object {
raw: string,
clientIp: string,
timestamp: string,
method: string,
target: string,
protocol: string,
status: string,
statusClass: StatusClass,
bytesSent: string,
referrer: string,
userAgent: string,
}
SysEnv = Object {
S3_ENDPOINT?: string,
...: string,
}
HealthRoute = HTTP::Route {
method: `GET`,
path: `/health`
}
LogsRoute = HTTP::Route {
method: `GET`,
path: `/logs`,
query: Object {
day?: LogDay,
}
}
Routes = HealthRoute | LogsRoute
log_bucket = `rank-example-logs`
default_log_day: LogDay = `2026-05-12`
available_log_days: [LogDay] = [`2026-05-11`, `2026-05-12`]
{ S3_ENDPOINT } = Env<SysEnv> {}
s3_endpoint = S3_ENDPOINT ?? `http://127.0.0.1:4566`
s3Client: aws::S3::ClientConfig = S3::createClient({
region: `us-east-1`,
endpoint: s3_endpoint,
credentials: {
accessKeyId: `test`,
secretAccessKey: `test`,
},
})
status_class_for = |status: string| -> StatusClass {
return startsWith(status, `2`) ? `2xx`
: startsWith(status, `3`) ? `3xx`
: startsWith(status, `4`) ? `4xx`
: startsWith(status, `5`) ? `5xx`
: `other`
}
log_key_for = |day: LogDay| -> string {
return match day {
`2026-05-11` => `nginx/2026-05-11/access.json`,
`2026-05-12` => `nginx/2026-05-12/access.json`,
}
}
parse_access_entries = |line: string| -> [AccessLogEntry] {
return match line {
parse`{clientIp:string} - {_remoteUser:string} [{timestamp:string}] "{method:string} {target:string} HTTP/{protocol:string}" {status:string} {bytesSent:string} "{referrer:string}" "{userAgent:string}"` => [{
raw: line,
clientIp,
timestamp,
method,
target,
protocol,
status,
statusClass: status_class_for(status),
bytesSent,
referrer,
userAgent,
}],
_ => [],
}
}
list_logs = |day: LogDay| -> HTTP::Response {
key = log_key_for(day)
fetched: aws::S3::ObjectResult<AccessLogFile> = aws::S3::Object<AccessLogFile> {
client: s3Client,
bucket: log_bucket,
key: key,
}
{ object, metadata } = fetched
non_blank_lines = object.raw
|> split(`\n`)
|> map(|line: string| trim(line))
|> filter(|line: string| !isBlank(line))
entries = non_blank_lines
|> flatMap(|line: string| parse_access_entries(line))
return {
status: 200,
body: {
availableDays: available_log_days,
source: {
bucket: log_bucket,
key: key,
server: object.server,
day: object.day,
requestId: metadata.requestId,
},
summary: {
totalLines: length(non_blank_lines),
parsedLines: length(entries),
droppedLines: length(non_blank_lines) - length(entries),
okResponses: count(entries, |entry: AccessLogEntry| entry.statusClass == `2xx`),
redirects: count(entries, |entry: AccessLogEntry| entry.statusClass == `3xx`),
clientErrors: count(entries, |entry: AccessLogEntry| entry.statusClass == `4xx`),
serverErrors: count(entries, |entry: AccessLogEntry| entry.statusClass == `5xx`),
},
entries: entries,
}
}
}
pub config = {
defaultResponseFormat: `json`
}
pub main = |req: Runtime::ExecutionContext<Routes>| -> HTTP::Response {
return match req {
HealthRoute => {
status: 200,
body: {
ok: true,
service: `s3-logs-example`,
},
},
LogsRoute => list_logs(req.query.day ?? default_log_day),
}
}
examples/s3-logs/rank.toml
manifestVersion = 1
[package]
name = "s3-logs-example"
version = "0.1.0"
source = "."
[providers]
aws = { path = "../../packages/plugins/aws" }
[security]
allow-provider-capabilities = ["network"]
allow-env = ["S3_ENDPOINT"]
Output
{
"availableDays": ["2026-05-11", "2026-05-12"],
"source": {
"bucket": "rank-example-logs",
"key": "nginx/2026-05-12/access.json",
"server": "edge-2",
"day": "2026-05-12"
},
"summary": {
"totalLines": 5,
"parsedLines": 5,
"droppedLines": 0,
"okResponses": 1,
"redirects": 0,
"clientErrors": 2,
"serverErrors": 2
}
}
Key concepts
aws::S3::Object<T>— reads the seeded object from S3 and validates the wrapper shape before any line parsing runs.parsestring patterns — the log parser uses a single structured-textmatcharm instead of separate regex capture indexing.- Pipe +
flatMap— the raw text is split, trimmed, filtered, and parsed through one left-to-right collection flow. Runtime::ExecutionContext<Routes>—GET /healthandGET /logsshare one route union with typed query narrowing forday.- Current S3 surface — the current AWS provider reads S3 objects as JSON only, so the seeded raw nginx text lives inside the
rawfield of a JSON object.
Run it
Start LocalStack from examples/s3-logs:
docker compose up
If another LocalStack setup is already using port 4566, choose a different host port:
LOCALSTACK_PORT=4567 docker compose up
Then start the Rank server from the repository root:
npm start -- serve examples/s3-logs --dev
If you changed the LocalStack port, pass the matching endpoint to the Rank server:
S3_ENDPOINT=http://127.0.0.1:4567 npm start -- serve examples/s3-logs --dev
Try it:
curl http://127.0.0.1:3000/health
curl http://127.0.0.1:3000/logs
curl 'http://127.0.0.1:3000/logs?day=2026-05-11'