skip to content

What does a per-operation GraphQL log line record, and what stays off it?

level: juniorimportance: should knowfreq 44%

answer

  1. One route makes every line identical
  2. Log what executed, not what arrived
  3. Type, name, hash, duration, error paths
  4. The payload never goes on it
  5. Keys not values, hash not text

basics

~20 s

A per-operation GraphQL log line records the operation type and name, a hash of the executed document, the duration, and the path of every error the response carried. Raw variable values and the full document text stay off it.

solid answer

~50 s

Because a GraphQL API is normally one route, a transport log line describes an enrolment mutation and a whole timetable read identically: same path, same method, usually `200`. A per-operation line replaces that with what the server actually executed - the operation type (`query`, `mutation`, `subscription`), the operation name as the client reported it, a hash identifying the document, the elapsed time, whether `data` came back partial, and the `path` of each entry in the `errors` array. What stays off is the payload: raw variable values, because they are user input and routinely carry identifiers, e-mail addresses and credentials, and the full document text, because it is large, repetitive and can itself contain literal arguments. Log the variable *keys* if you need shape, and the hash instead of the text. None of this is in the GraphQL specification; it is an operational convention.

code

json · 13 lines
json
{
  "correlationId": "7f31c0a4-2b19-4c8e-9d67-1a5be0c43e02",
  "operationType": "mutation",
  "operationName": "EnrollInSection",
  "documentHash": "sha256:9c14f0b7a2",
  "durationMs": 148,
  "dataPartial": true,
  "errorCount": 1,
  "errorPaths": ["enrollments.0.section.instructor"],
  "variableKeys": ["sectionId", "studentId"],
  "clientName": "enrolment-mobile",
  "clientVersion": "7.3.1"
}

go deeper

for a junior

Be ready to list the fields off the top of your head: operation type, operation name, document hash, duration, error paths. Then say plainly that raw variable values and the full document text do not belong on the line.

for a middle

Explain why each field is there rather than reciting them - why the hash and not the name identifies the document, why the error paths matter when the status is always 200, and why redaction must be an allowlist rather than a denylist.

for a senior

Show you have operated this. Talk about a correlation id shared with the response, keeping the hash-to-document map access-controlled, and how retention and who-can-read-logs shape what you are willing to put on the line at all.

for a principal

Own the policy: which fields every service must emit so cross-service queries work, how sensitive arguments are marked in the schema so redaction is automatic rather than remembered, and the cost and retention envelope the whole scheme has to fit in.

## Why the transport line is empty An HTTP-shaped API leaves a legible trail: a distinct path per resource, a method that says what was attempted, a status that says how it went. A GraphQL API generally throws all three away. In a course-enrolment graph, `POST /graphql 200 41ms` is the log line for a student opening their timetable, and `POST /graphql 200 41ms` is also the log line for the mutation that enrolled them in a section and took a seat off the waitlist. A 4-person platform team on call at 02:00 cannot start an investigation from that. So the unit of logging moves from the request to the **operation**: one line per operation the server executed, written after execution finishes, describing what was run rather than how it arrived. ## The identity fields **Operation type.** `query`, `mutation` or `subscription`. It is taken from the document the server parsed, not from anything the client asserts separately, and it is the cheapest useful split there is: reads and writes stop sharing a latency distribution, and a subscription - which may live for hours - stops being mistaken for a very slow query. **Operation name.** The name written on the operation in the document, echoed by the request's `operationName` parameter when a document holds more than one operation. It is the human handle: `StudentTimetable`, `EnrollInSection`. It is authored by the client and nothing on the server binds it to any particular document, so it goes on the line as a *label*, never as the identity. **Document hash.** A digest of the executed document - conventionally computed by the server over a normalized form of it. This is the field that actually groups lines together, and it is the same idea Automatic Persisted Queries keys on when a client sends a SHA-256 hash of the document text instead of the text itself. Two lines with one hash ran the same document; that is a guarantee the name cannot give. ## The outcome fields **Duration**, measured around execution. **Error paths.** A GraphQL response is `data` plus `errors`, and an error raised while resolving a field carries a `path` - the response path to the field that failed, such as `["enrollments", 7, "section", "instructor"]`. Those paths, plus the count, are the diagnostic core of the line: they say *where in the result* it broke, which no status code can. **Partiality.** Whether `data` came back complete, partially filled, or `null`. A `200` with holes in it is the normal failure mode of this protocol, and if the line does not say so, nothing does. **A correlation id.** One value shared by this line, any downstream lines the request produced, and - by convention - an entry the server puts in the response `extensions`, so a support ticket carrying a screenshot can be turned into a log query. ## What must stay off **Raw variable values.** This is the mistake the question exists to catch. Variables are the arguments to the operation, and in this graph that means student identifiers, e-mail addresses, dates of birth, and - in an authentication mutation - a password or a token. Logs are copied, indexed, retained and read by more people than the database is. Log the variable *names* and, if you need shape, their types or sizes; redact the values, ideally driven by the schema so a field marked sensitive is never rendered. Redaction has to be positive - an allowlist of keys you print - because a denylist is one new argument away from leaking. **The full document text.** It is long, near-identical between requests, and it can carry the very data the variables were supposed to keep out: nothing stops a client inlining a literal argument instead of using a variable, and a document written that way puts the value in the text. The hash is the substitute, with a hash-to-document map kept somewhere separate and access-controlled. **Whole request headers.** Cookies and `Authorization` values are credentials; a logged bearer token is a live one until it expires. ## What it buys With those fields present, the questions a team actually asks become one query each: which operations are slowest, which document hash started erroring at 09:14, which response paths fail, and whether a spike is one client's document or all of them. Without them you have a wall of identical `200`s. One caveat worth saying out loud in an interview: none of this is specified. The GraphQL specification defines the type system, validation and execution and says nothing about logging; the GraphQL over HTTP specification is a working draft covering request and response shape. Per-operation logging is a convention, and the exact field names differ everywhere.

  • If you must not log variable values, how do you reproduce a failing operation later?
    You reproduce it from the document plus the *identifiers* you are allowed to keep. The hash resolves to the document text through a separate, access-controlled store, and the line carries enough safe context - the correlation id, the authenticated principal id, the error paths - to find the same rows again. Anything genuinely needed for a repro is captured under an explicit allowlist with its own retention, not by dumping the variables into the general log stream.
  • Why log the variable names when the values are redacted?
    The names carry shape without carrying data. They tell you which arguments the client actually supplied, which distinguishes a call that passed an optional filter from one that omitted it, and they make a document's usage legible when the same operation is invoked several ways. They are authored in the document rather than typed by a user, so unlike the values they carry no personal data - though an argument named after the value it holds is a reason to fix the schema, not to log it.
  • Where does the line get written - before or after execution?
    After. The fields that make it useful only exist once execution finishes: the duration, the error count and paths, whether `data` came back partial. Emitting on entry gives you a line with nothing on it but a start time, and a request that dies mid-execution then leaves two half-records to be joined. If you need arrival evidence for hung requests, keep the single completion line and rely on a separate timeout path rather than splitting every operation in two.

The transport log is the postmark on an envelope - it proves something arrived, and every envelope's postmark looks alike. The per-operation line is the delivery receipt: what was in the parcel, who it was for, and which items arrived damaged.

saying these in an interview costs you the question

  • Logging the full variables object for debuggability
  • Logging the whole document text on every request
  • Assuming HTTP status tells you the outcome
  • Treating the operation name as the document identity
  • Redacting with a denylist of known-sensitive keys
  • Believing the GraphQL spec defines log fields

context