Skip to content
carameldocs · 0.16.0
Browse documentation
Reference / Crema

See what your application did.

Crema is Caramel’s observability: request ids, one log line for every request, job and schedule run, error reports, an owner-only ops socket, an opt-in recorder and exporter, and the development tools.

ContractValue
Release0.15.0
NeedsA Caramel application; the recorder and exporter are opt-in requires
CoversRequest ids, the log line, error reports, the ops socket, metrics, the recorder, the OTLP exporter and the development tools
Proved byfrappe traces --agent · frappe trace last --md · frappe errors --agent

Learning it for the first time? The guide Know what your application did walks through it in order. This page lists every name, field, variable and limit.

Limits at a glance

LimitValue
Inbound X-Request-ID8–128 characters
Slow request, default1 second
Slow job or schedule run, default5 seconds
Slow query, default100 milliseconds
Runs that make a repeated statement5
Spans in a trace (more are counted as dropped)200
Distinct statements tracked for repeats in a trace500
Span detail kept in the trace ring2,048 bytes
Log entry kept as a span1,000 bytes
Binds kept per statement, each cut at20, 200 bytes
Copy with values literal; longer stays $n2,000 bytes
Error report backtrace50 frames of 2,048 bytes
Error report message8,192 bytes
Causes listed5
Error ring (least recently seen is evicted)500 fingerprints
Trace ring200 traces
/v1/traces and /v2/traces limitdefault 50, at most 200
Concurrent tails (events buffered for each)8 (1,000)
Tail keep-alive : pingevery 15 seconds
Ops connection read and write timeout10 seconds
POST /v1/debug-tokens minutes1–120, default 15
POST /v1/debug-tokens body1 KiB
Debug token lifetime2 hours
Distinct request series (the rest fold into (other))1,000
Recorder keys of each kind per flush500
Recorder SQL key500 bytes
Recorder flush interval15 seconds
Recorder retention (pruned hourly)7 days
Recorder histogram12 buckets: 5, 10, 25, 50, 100, 250, 500, 1,000, 2,500, 5,000, 10,000 ms and overflow
APP insights rows20
Exporter queue2,048 traces
Exporter batchevery 5 seconds or at 512 spans
Exporter timeouts5 seconds connect, 10 seconds read
Error page frames80
Source file shown in the error page1 MiB
dump span8,192 bytes
Inspector traces listed500
Toolbar request listlast 10
frappe traces --limit default20
Frappé keeps500 traces, 200 standalone errors, 50 builds
events.jsonl rolls at8 MiB
Trace or request id prefixat least 6 characters
Database statement and lock timeout, default30 s, 5 s (migrate: none, 5 s)
auto_explain threshold250 ms
access.log rolls at1 MB
Collector body rejected over4 MiB
Collector memory2,000 traces, 200 spans each, 32 attributes per span

Crema runs in every application

Crema is part of every application. What it does depends on how the process runs and which parts you opt into.

ModeWhat runs
Always onA request id and trace for every routed request, job and schedule run; the canonical log line; error reports; the in-memory error and trace rings; the metrics behind ops metrics. A serve process also opens the ops socket.
Opt inrequire "caramel/crema/recorder" keeps latency history in the database; new applications already have it. require "caramel/crema/otlp" exports traces to a tracing backend.
Development onlyCode compiled with -D caramel_development, which frappe dev, frappe check and one-shot development builds pass, and run with CARAMEL_ENV=development: the error page, dump, Server-Timing, query binds and source lines, and the event sink behind the inspector.

Settings are set before the application starts

Set these in config/application.cr, before the application starts.

SettingMeaning
Caramel::Crema.slow_requestTime::Span, default 1 second. A request at least this slow is kept in the trace ring as slow and flagged in the toolbar.
Caramel::Crema.slow_jobDefault 5 seconds. Applies to jobs and schedule runs.
Caramel::Crema.slow_queryDefault 100 milliseconds. Each statement at least this slow counts in the log line’s slow_queries.
Caramel::Crema.on_error { |report| … }Called for every error report, handled or not, with the redacted message and backtrace.
Caramel::Crema.report(error, handled: true, source: nil)Reports an error you rescued. It never raises. A handled report writes a WARN line and leaves the trace’s outcome ok; handled: false marks the trace failed.
Caramel::Crema.measure(kind, name, detail = nil) { … }Times a block as a span of the current trace. Kinds: sql, http, view, enqueue, log, dump and domain.
Caramel::Crema.command(name, syntax) { |arguments| … }Adds a command to the application binary and returns its exit code. The first word of the syntax is the name; registering a name again replaces the earlier command. The binary’s usage text lists the syntax.
Caramel::Crema.console_page(segment) { |request| … }Adds a page at /segment to the ops console. The block returns HTML.
Caramel::Crema.subscribe(sink)Adds a sink that receives finished traces and error reports. Remove it with unsubscribe.
Caramel.dump(value)Development only. See below.

Environment variables choose mode, format and socket

VariableMeaning
CARAMEL_ENVdevelopment adds request paths and error messages to log lines. In a development build it also turns on the error page, dump, Server-Timing, query binds and the literals behind Copy with values. test and development default the log format to text. Unset counts as production.
CARAMEL_LOG_FORMATjson or text. Any other value falls back to text in development and test, JSON elsewhere.
LOG_LEVELDefault Info. Applies to every log source, not only crema.
CARAMEL_OPS_SOCKETA path for the ops socket, or off. Unset, a serve process uses its CARAMEL_SOCKET path with .sock replaced by .ops.sock. A work process opens a socket only when this names a path. The ops client reads the same variables.
CARAMEL_EDITORAn editor preset or a link template. See the development section.
CARAMEL_PROJECT_ROOTThe project root, default the current directory. Error locations are shown relative to it, and its app, config, src and db folders count as your code.
APP_SECRETThe application secret. Debug tokens are signed with a key derived from it, so rotating it ends every token.
CARAMEL_DEV_EVENTSSet by frappe dev for development builds: the socket the application sends its events to. Do not set it yourself.
CARAMEL_STATEMENT_TIMEOUT, CARAMEL_LOCK_TIMEOUTPostgreSQL’s statement_timeout and lock_timeout on every pooled connection of every command but migrate: whole seconds, 0 to 86400, default 30 and 5. migrate reads CARAMEL_MIGRATION_STATEMENT_TIMEOUT and CARAMEL_MIGRATION_LOCK_TIMEOUT, default 0 and 5. 0 means none. Cold Brew’s pool reads the runtime pair.
CARAMEL_POOL_SIZE, CARAMEL_POOL_TIMEOUTConnections a command’s pool holds, 1 to 32, default 4, and seconds a checkout waits for a free one, 1 to 300, default 5. An invalid value exits 1 with one line naming it.
OTEL_*Seven variables read by the exporter; see below.
ConditionOutcome
CARAMEL_ENV set to anything but development, test or productionstops every database command with CARAMEL_ENV must be development, test or production
A timeout variable that is not a whole number of seconds from 0 to 86400stops the command with KEY must be a whole number of seconds from 0 to 86400
A statement past statement_timeoutis cancelled by PostgreSQL, which reports it before the pool’s own read timeout

Every traced response carries a request id

ConditionOutcome
Inbound X-Request-ID of 8 to 128 characters from A-Z a-z 0-9 . _ : -kept
Any other inbound X-Request-IDreplaced by a UUID
Static files, and the 400 and 421 refusalsnot traced, and carry no id
traceparent with version 00, lowercase hex, a 32-digit trace id, a 16-digit parent id and flagscontinues the caller’s trace and supplies the log line’s trace_id and parent_id; the first flag bit says whether the caller sampled it
A traceparent with all-zero idsignored
enqueuestores the traceparent, the request id and the debug flag in caramel_jobs.context, never a message; the job continues that trace
A schedule run, or a missing or malformed job contextstarts a new trace
Each Caramel::Outbound callis an http span named METHOD host, and sends a traceparent unless you set one
A debug tracesends X-Caramel-Trace with the trace id

Every finished trace writes one log line

The crema log source writes one INFO line for each finished trace, named request, job or schedule.

FieldPresent when
nameAlways. GET /books/:id, the job class or the schedule name. An unrouted request is GET (none); a method outside GET, HEAD, POST, PUT, PATCH, DELETE and OPTIONS is OTHER.
request_id, trace_id, parent_idThe ids above. parent_id only with a valid inbound or stored traceparent.
duration_ms, outcomeAlways. outcome is error after an unhandled error or a 500, or when a job’s perform raised, even if it will retry.
method, route, action, status, bytes, streamedRequests. route and action only when a route matched; bytes is left out for a streamed response.
pathRequests, only when CARAMEL_ENV=development.
job_id, queue, attempt, queue_lag_msJobs. The lag is the time past run_at, never negative.
db_count, db_ms, db_wait_msStatements, their time, and the wait for a pooled connection.
view_ms, outbound_count, outbound_msThe outermost view’s time and the Caramel::Outbound calls.
cache_hits, cache_misses, enqueuedCaramel::Cache reads and jobs enqueued.
slow_queries, repeated_queriesStatements past slow_query, and distinct statements that ran five or more times. Repeats are counted only when the trace records spans: the ops socket, the recorder or the exporter is on, or the build is for development. A trace keeps at most 200 spans (more are counted as dropped) and tracks at most 500 distinct statements for repeats.
debugtrue for a debug trace.
error_class, fingerprintOnly when the trace ended in an unhandled error.
ConditionOutcome
A field with no valueis left out
Any log entry written inside a tracealso carries trace_id and request_id
A log entry written inside a trace that records spansis kept as a log span of up to 1,000 bytes
A slow request or jobshows in the trace ring, the toolbar and the inspector; there is no slow field. A slow statement counts in slow_queries
FormShape
error lineOne error entry for each report: WARN when handled, ERROR when not. Fields: error_class, fingerprint, location (relative file:line), source (the trace name, or stream or cold_brew.worker, .maintenance, .scheduler, .hooks NAME), handled, request_id and trace_id. The message is added only when CARAMEL_ENV=development.
JSONOne object per line: ts (UTC, milliseconds), lowercase level, source, msg, then the fields. A field named like one of those four is written as data_NAME, so an error’s source becomes data_source. The default outside development and test.
textHH:MM:SS.mmm LEVEL and a one-line summary with one decimal per time: 12:00:00.123 INFO request GET /books/1 200 12.4ms db=3/4.1ms view=2.0ms App::Books::Show request_id=8b1f; outside development the route /books/:id stands where the path is. Values with spaces are quoted. The default in development and test.

Error reports are redacted and stay in memory

PartContent
Fieldserror_class, message, backtrace (at most 50 frames of 2,048 bytes), fingerprint, location, handled, source, request_id, trace_id, occurred_at and the classes of up to five causes. A message is cut to 8,192 bytes.
FingerprintThe first 12 hex digits of a SHA-256 over the error class, the path of the first frame in your own code and its method name. With no such frame, the first parseable frame; with none, the class alone. Lines, columns and messages are never hashed.
RedactionApplied to every message and backtrace frame: values of environment variables whose names contain SECRET, PASSWORD, TOKEN, API_KEY or DATABASE_URL become [redacted]; PostgreSQL URLs become [database URL redacted]; name=value, name: value and JSON pairs whose name holds password, secret, token, api key, access key, private key, credential or authorization, and bare Bearer tokens, become [credential redacted]. If redaction fails the report keeps its class and the message [unavailable].
Where it livesA report’s message and backtrace exist in memory: the ring behind ops errors, and development surfaces. Crema’s logs, tail events, metric labels, exports and caramel_metrics carry the class and fingerprint, never the message. The ring holds 500 fingerprints; the least recently seen is evicted.
ConditionOutcome
A job’s failed runis written unredacted by Cold Brew to caramel_jobs.last_error: the class, the message and the first 12 backtrace lines; APP jobs show ID prints it
A process restartclears the error and trace rings; forward Crema.on_error reports to an error tracker for history
Not stored Messages and backtraces never reach logs, exports or the recorder.

They stay in memory and in development surfaces. Only caramel_jobs.last_error, which Cold Brew writes, keeps a job’s message on disk.

Every statement in a trace starts with a tag

Every statement run inside a trace starts with a comment: /*action='App%3A%3ABooks%3A%3AShow'*/ for a request, /*job='…'*/ for a job and /*schedule='…'*/ for a schedule. The value is the action or job class or the schedule name, percent-encoded. Pools name themselves in application_name: caramel-web for serve, caramel-cold-brew for workers and caramel-listen for PubSub.

ConditionOutcome
A statement run outside a tracecarries no tag
A statement’s binds in a development buildare kept for the inspector: at most 20, each cut at 200 bytes, NULL for nil
A span’s detail in the trace ringis cut at 2,048 bytes
A statement over slow_querycounts in the log line’s slow_queries

The ops socket answers its owner only

An HTTP/1.1 server on a Unix socket, for the application’s owner.

ConditionOutcome
A directory that is not a real directory you own with no group or other permissionsstays off: the application keeps serving, and the log says why
The socket fileis mode 0600
A live socketis never replaced; a dead one is
Each connectionhas a 10-second read and write timeout
A tail with nothing to send for 15 secondssends : ping to keep the connection open
A request whose Host is not ops, localhost, 127.0.0.1 or [::1]answers 421
A write with an Origin headeranswers 403
A wrong methodanswers 404
A ninth concurrent tailanswers 429
POST /v1/debug-tokens without an application secret, which a work process lacksanswers 404
Every answeris JSON with a version and carries Cache-Control: no-store
EndpointAnswers
GET /v1/statusApp, role, uptime, environment, in-flight work, request counts and p95, memory, database pools, workers, scheduler, sinks and dropped events.
GET /v1/requests, /v1/fibersIn-flight requests and jobs; fibers grouped by name.
GET /v1/metricsPrometheus text.
GET /v1/tailServer-sent events of finished traces and errors. Query: errors=1, slow=MS, logs=1. At most 8 at once, 1,000 events buffered for each. A debug trace always passes the filters. With logs=1 the events include log lines exactly as written to stdout.
GET /v1/errors, /v1/errors/FINGERPRINTErrors since the process started, grouped, newest first. The second route is the only one that returns a message and backtrace.
GET /v1/traces?limit=N&reason=error|slow|debugThe trace ring, 50 by default, at most 200. A trace is kept when it failed, was slow or carried a debug token. The ring holds 200 traces.
GET /v1/traces/REFOne trace with its spans. REF is a prefix of at least 6 characters of a trace id or request id.
POST /v1/debug-tokensJSON {"minutes": N}, 1 to 120, default 15, in a body of at most 1 KiB.

The read-only console is the same data as HTML at /, /requests, /tail, /jobs, /database, /errors, /traces and /insights. It can neither retry a job nor issue a token, and its content security policy allows only its own scripts and styles.

Binary commands exit 0, 1 or 2

The commands page lists each one’s syntax; these are the details. Run ops where the process runs; --socket=PATH works on every subcommand.

See every command →
CommandDoes
ops statusSeven lines: App, Requests, Jobs, Database, Memory, Fibers and Crema. --json prints the status document.
ops requests, ops fibersIn-flight work; fibers by name.
ops metricsPrometheus text, as served.
ops tailStreams until you stop it. --slow takes a value here, and MS must be an integer.
ops errorsFingerprint, count, last seen, class, location and source.
ops error FINGERPRINTOne error with its redacted message and backtrace. A miss says that restarts clear the rings.
ops tracesOne reason flag; errors win over slow, slow over debug. The reason flags take no value.
ops trace REFOne trace as text, or Markdown with --md.
ops debug-tokenPrints the token, its expiry, a curl line with the header and a browser line that sets the cookie.
ops consolePrints ssh -N -L 8765:SOCKET HOST and http://localhost:8765/. It does not connect.
jobs, jobs statsEach queue’s running, retrying, queued, scheduled and failed jobs, the finished in the last hour and the age of the oldest due job.
jobs failedFailures grouped by job class and error class, newest first; --limit defaults to 20.
jobs show IDEvery column of one job, including last_error and context. It prints to your terminal only.
jobs retry ID, jobs retry --class=NAMEWrites to the database: clears the failure and runs the job once more, an abandoned job included. It keeps attempts.
jobs unknownThe queued job classes this binary does not compile, one per line, for a deploy to check before the binary takes over.
db diagnoseSections connections, long_running (over 1 second), blocking, cache_hit, seq_scans, unused_indexes, vacuum, table_sizes and outliers (the slowest statements of your role, from pg_stat_statements).
db roles DATABASE MIGRATION_ROLE RUNTIME_ROLEPrints a psql script that creates the two login roles, the database the migration role owns and the grants Latte gives a site: the runtime role reads and writes rows through default privileges. It sets no passwords, grants both roles to the role that runs it, and needs no database. Names match [a-z_][a-z0-9_]*, up to 63 characters.
insightsThe recorder’s top 20 rows by count under the headers KEY COUNT ERR P50MS P95MS MAXMS TOTAL_S. --since defaults to 1h, --kind to request.
ConditionOutcome
Exit status0 is success, 1 a failure or nothing found, 2 a usage error
ops with no socket at the pathexits 1 with No ops socket at PATH. Is the application running with CARAMEL_OPS_SOCKET?
ops errorsshows UTC times; ops traces and ops tail show local times
ops traces --slow=500, a reason flag with a valueexits 2, a usage error
ops tail --slow without a valueexits 2, a usage error
jobs, db diagnose and insightsneed DATABASE_URL and no running application
jobs with CARAMEL_ENV=testrefuses
insights without the recorderexits 1 with The Crema recorder is off. Add require "caramel/crema/recorder" to config/application.cr.
insights with no rows in the windowprints No aggregates in the last 1h. Is the recorder running? and exits 0
jobs unknown when any class is unknownexits 1; with no caramel_jobs table yet, it prints nothing and exits 0
db diagnose with an unreadable sectionsays so and the rest continue; exits 0 if any section was readable
db roles with a name that does not match [a-z_][a-z0-9_]* up to 63 charactersexits 2

Metrics count from the process start

ConditionOutcome
A request routeis a route template; an unmatched route is (none)
More than 1,000 distinct request seriesfold into (other)
Valuescount from the process start and are not stored
Durationsare histograms in seconds with bounds from 0.005 to 10 and +Inf
MetricType and labels
caramel_requests_totalcounter: method, route, status.
caramel_request_duration_secondshistogram: method, route.
caramel_jobs_totalcounter: job, outcome.
caramel_job_duration_secondshistogram: job.
caramel_job_queue_lag_secondshistogram: queue.
caramel_schedules_totalcounter: schedule, outcome.
caramel_errors_totalcounter: error_class.
caramel_inflightgauge: kind is request, job or schedule.
caramel_gc_heap_bytes, caramel_gc_free_bytesgauges for the Crystal heap.
caramel_gc_allocated_bytes_totalcounter of bytes allocated.
caramel_fibersgauge: fibers alive.
caramel_db_pool_connectionsgauge: pool, state is open, idle or in_flight.
caramel_db_pool_maxgauge: pool, the pool’s limit.
caramel_crema_dropped_totalcounter: sink, events a sink lost.
caramel_build_infogauge, always 1: app, caramel.
caramel_process_start_time_secondsgauge: when the process started.

Debug tokens expire within two hours

A token is EXPIRY.SIGNATURE: the expiry as Unix seconds and an HMAC-SHA-256 of it, keyed from the application secret.

ConditionOutcome
A tokenis valid until its expiry and never longer than two hours, whatever it claims
The X-Caramel-Debug header and the __Host-caramel_debug cookie both sentthe header wins
An invalid X-Caramel-Debug headeris not replaced by the cookie
A valid tokentraces the request in full, answers with X-Caramel-Trace, keeps it in the trace ring with the reason debug, always exports it when the exporter is on, and passes it to the jobs it enqueues
A token that leaksstays valid until it expires, because the signature covers only the expiry

The recorder stores aggregates, never a path or a message

ConditionOutcome
require "caramel/crema/recorder"aggregates each finished trace in memory and writes to caramel_metrics
Upgradingrun frappe migrate, since caramel_metrics comes with Cold Brew’s framework tables
What is storedroute templates, job classes, schedule names, parameterized SQL and outbound hosts, never a path, a bind value, a message or a trace
More than 500 keys of one kind per flushfold into (other)
A failed flushloses its batch, writes one warning and counts toward caramel_crema_dropped_total
NameMeaning
caramel_metricsbucket (the start of the minute), kind, key, count, errors, total_ms, max_ms and histogram, a 12-bucket latency count with bounds 5, 10, 25, 50, 100, 250, 500, 1,000, 2,500, 5,000 and 10,000 ms, and overflow. The primary key is (bucket, kind, key). Rows merge by adding counts and keeping the larger maximum.
kindrequest, job, schedule, sql or outbound.
SQL keysQuoted literals and bare numbers become ?, IN (…) lists become IN (?), whitespace collapses, and the key is cut to 500 bytes.
Caramel::Crema::Recorder.flush_intervalDefault 15 seconds. Each flush upserts into the current minute.
Caramel::Crema::Recorder.retentionDefault 7 days. Pruned hourly.

The exporter sends nothing until an endpoint is set

require "caramel/crema/otlp" exports each sampled request, job and schedule run as OTLP over HTTP with JSON bodies, with a child span for every query and outbound call.

ConditionOutcome
No endpoint setnothing is sent
Protobufnot supported
An OTEL_* variable other than the seven listednot read
OTEL_EXPORTER_OTLP_PROTOCOL other than unset or http/jsonlogs a warning and leaves export off
An unknown OTEL_TRACES_SAMPLERwarns and uses traceidratio
A parent-based samplerfollows the caller’s flag when there is one
A failed trace that sampling skippedis exported as its root span
A debug traceis always sampled
A failed batchis dropped, counted, and logged at most once a minute
VariableMeaning
OTEL_EXPORTER_OTLP_ENDPOINTThe base URL; /v1/traces is appended.
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTUsed as given, and wins over the base.
OTEL_EXPORTER_OTLP_PROTOCOLUnset or http/json.
OTEL_EXPORTER_OTLP_HEADERSComma-separated name=value pairs, percent-decoded.
OTEL_SERVICE_NAMEDefault: the application’s title.
OTEL_TRACES_SAMPLERalways_on, always_off, traceidratio (the default), parentbased_always_on, parentbased_always_off or parentbased_traceidratio.
OTEL_TRACES_SAMPLER_ARGThe ratio, 0 to 1; default 1.
PartBehaviour
SpansA request is a server span, a job a consumer span and a schedule an internal span; queries and outbound calls are client spans. log and dump spans are never exported. Attributes are route templates, db.query.text with parameterized SQL, the HTTP method and status, the job’s queue, id and attempt, and caramel.request_id. A failed trace adds the exception’s class only.
SamplingDecided once per trace.
DeliveryUp to 2,048 traces wait in memory and go out every 5 seconds or at 512 spans, with a 5-second connect and 10-second read timeout.

Development tools need a development build

ConditionOutcome
A binary built without -D caramel_development, or run without CARAMEL_ENV=developmentholds none of these tools; a check confirms a production binary holds none of it
An unrecognised CARAMEL_EDITORfalls back to zed
Server-Timing on a streamed responseis not sent
ToolBehaviour
Error pageAn unhandled error answers 500 with the class, the redacted message, up to 80 frames with Open in editor links, four lines of source either side of your first frame (files under app, config, src or db up to 1 MiB), the request, the queries before the error and the causes. A client that asks for JSON first gets an error object with the class, message, location, request and trace ids and backtrace instead.
dump(value)Prints dump file:line: value to standard error and returns the value; inside a trace it adds a dump span of up to 8,192 bytes. In actions, views and jobs, and as Caramel.dump. In other builds it returns the value untouched.
Server-Timingdb;dur=…;desc="N queries", view;dur=…, total;dur=….
Toolbarfrappe dev adds it to full HTML responses that carry an X-Request-ID, not to htmx partials, the build-error page or the inspector. It shows the route, status, time and query count, flags for errors, slow requests and repeated queries, and the last ten requests. Each part links to #timeline, #queries or #error on the request’s page (the Timeline and Queries sections are on the page even when empty and say so; the Error section is there for a failed request). The minimise button shrinks it to a dot, and the dot restores it. ` opens and closes the list, j and k move in it, Esc closes it; shortcuts are ignored in fields and with a modifier key. Copy for an agent fetches /__caramel/dev/trace.md?id=ID (a hexadecimal id of 6 to 32 characters, development session only). Theme cycles auto, light and dark. It polls every 400 ms and reloads when the build changes.
Inspector/__caramel/dev/inspector lists up to 500 traces, filtered by ?only=errors, slow or jobs. Beside it: /inspector/traces/ID (a prefix of at least 6 characters), /inspector/errors and /inspector/builds. Each query on a trace page has Copy SQL and Copy with values; the values come from literals built when the query ran (numbers and booleans bare, text, times and bytes quoted, nil as NULL, arrays as ARRAY[…]) and never leave development. Placeholders without a value stay as $n. The page follows the system’s light or dark appearance; the Theme button sets it for the toolbar and the inspector together (localStorage key caramel.dev.theme). Its endpoints answer GET only and check the host.
CARAMEL_EDITORA preset or a template with {path}, {line} and {column}. Presets: zed (the default), vscode, cursor, sublime, textmate and idea. frappe dev also reads it from the project’s .env.
Caramel/Dumpfrappe lint reports any call to dump without a receiver or on Caramel, including your own method of that name. The rule is on in new applications’ .ameba.yml.

Frappé commands replay kept events

All of these run inside an application and need no running frappe dev: they replay the events it kept. A value needs =: --limit=5, not --limit 5.

CommandDoes
frappe traces [--errors] [--slow=MS] [--limit=N] [--agent|--human]Recent requests, jobs and schedule runs, newest first. --slow=MS keeps traces of at least that many milliseconds; --errors keeps failed ones. --limit defaults to 20. --agent ends with OK traces N.
frappe trace REF [--md]One trace, or an error outside any trace. REF is last, last-error or a prefix of at least 6 characters of a trace id, a request id or the fingerprint of the error the trace ended with, or of an error outside a trace. --md adds sections for the error, backtrace, queries, logs, dumps and, when Latte holds spans from other services, Across services.
frappe errors [--agent|--human]Errors and repeated queries since the newest successful build, grouped.
frappe logs [app|compiler|access] [--follow]A site log through tail; access is the proxy’s. The Local environment page gives the line count and the folder.
frappe db diagnoseBuilds a development binary and runs its db diagnose against the development database.
ConditionOutcome
frappe tracesalways exits 0
frappe trace REF with no matchexits 1
frappe errors --agent with any groupprints one MRDP block each and exits 1; otherwise it prints OK errors 0
frappe logs with a missing logexits 1
Stored eventsthe newest 500 traces, 200 standalone errors and 50 builds, in events.jsonl in the site’s log folder; at 8 MiB it rolls into one .previous file
DiagnosticMeaning
ERR RUNTIME:STATUSOne per error group since the newest good build, :500 for requests. MSG gives the class, message and count; FIX points at frappe trace FINGERPRINT --md.
ERR REPEATED_QUERYOne per route and statement that ran five or more times in one request, with the most runs shown. FIX suggests preloading the association.

Latte adds statistics, an access log and a collector

PartAdds
PostgreSQLLatte’s PostgreSQL loads pg_stat_statements (top-level statements, no utility commands) and auto_explain (plans of statements over 250 ms, without timings or parameters, in postgres.log). The caramel_stats schema exists in the development database only, and frappe db dump leaves it out.
Access logCaddy writes one JSON line for each request to the site’s access.log, mode 0600, rolling at 1 MB with one previous file. It is kept out of proxy.log.
CollectorLatte listens on 127.0.0.1:4318 for POST /v1/traces with JSON only. It rejects bodies over 4 MiB, keeps the newest 2,000 traces in memory, 200 spans per trace and 32 attributes per span, and forgets them when it stops. If the port is busy, frappe services reports the collector as unavailable and everything else works.
Control API 2GET /v2/status adds the collector’s state; GET /v2/traces?limit=N (default 50, at most 200) and /v2/traces/ID read the collector; GET /v2/sites adds each site’s error count and last error, which holds a class, a location and a fingerprint, never a message.
Menu barLatte.app refreshes every 10 seconds. It shows each site’s error count beside its state and notifies on a new error fingerprint or a build failure. A notification needs permission and opens the inspector.

Prove it

Replay what the session kept

Serve the application, open a page in the browser, then read the events frappe dev kept. Caramel’s own scripts/check crema proves the production side: the JSON request line without a path, the X-Request-ID echo, the traceparent continuation, an error line without the message, the 0600 ops socket, the 421 on a bad Host, the metrics series and the debug token.

Terminal · inside the application
frappe dev --no-open
frappe traces --agent
frappe trace last --md
frappe errors --agent

Passing: frappe traces --agent prints one line per trace, starting request GET /, then OK traces N, and exits 0. frappe trace last --md prints the newest trace as Markdown. frappe errors --agent prints OK errors 0 and exits 0 when nothing failed since the newest successful build; a runtime error prints an ERR RUNTIME:500 block and exits 1, and frappe trace last-error --md then has a ## Backtrace section.