In Rego, what does print() do in a rule body and where does its output go?
answer
- A debugging aid, not part of the result
- Adding it cannot change the decision
- Watch which stream it lands on
- Undefined operands do not kill the body
- stderr, and -v under opa test
basics
~20 sprint() is Rego's debugging built-in. It writes its arguments to the stderr of the tool evaluating the policy and always succeeds, so adding it never changes a decision. The JSON result on stdout never contains it.
solid answer
~50 s`print()` takes any number of arguments of any type, joins them with spaces and writes one line to the evaluating tool's stderr. It always evaluates to true, so dropping it into a body cannot make a rule undefined or alter the result — and unlike ordinary built-ins it tolerates an undefined argument, printing `<undefined>` for that operand instead of killing the body. That tolerance is what makes it the right tool for finding where a body stops: print the field you think exists and see whether it is really there. With `opa eval` and `opa run` the line goes to stderr, so piping stdout into `jq` loses it; under `opa test` you need `-v` to see it. Do not treat it as production logging: when OPA is embedded in a host application, print statements are compiled out unless that host explicitly enables them and supplies a print hook.
go deeper
Be ready to say what print does, that it always succeeds, and that its output goes to the tool's stderr rather than into the decision JSON. Knowing you need -v under opa test is a good concrete detail to have.
Explain why print is exempt from undefined propagation and why that exemption is what makes it useful for finding a missing field. Contrast it with trace(), which emits a note into the explanation stream instead.
Show you know where print stops working: server deployments and embedded evaluation, where it either goes to a stream nobody reads or is compiled out entirely. Demonstrate the habit of converting a useful print into a decision message people can act on.
Own the boundary between debugging aids and operational visibility. Print is a laptop tool; the organisation's answer to why a rule fired has to be in the decision payload and the decision log, not in a developer's terminal.
## What `print` is for Rego has no debugger and no step execution. A rule either produces a value or it does not, and when it does not you get silence rather than a stack trace. `print` exists to break that silence: it is a built-in function you drop into a rule body to see the values the evaluator actually has at that point. ``` warn contains msg if { some name, res in input.Resources print("resource", name, "type", res.Type) ... } ``` ## Three properties that make it safe **It is always true.** Every expression in a rule body must hold for the body to succeed. `print` always succeeds, so inserting it never changes whether the rule fires or what it returns. You can add and remove it freely while chasing a problem. **It is variadic and untyped.** It accepts any number of arguments of any type — strings, numbers, objects, sets — and renders them separated by spaces on one line. There is no format string; if you want structure, print a small object. **It tolerates undefined operands.** This is the property that matters most and the one candidates miss. A normal built-in called with an undefined argument makes the whole expression undefined, which kills the body — so the naive attempt to debug an undefined rule by calling something on the suspect value just moves the silence around. `print` instead renders that operand as `<undefined>` and carries on. So `print("policy:", res.Properties.SslPolicy)` tells you directly whether that path exists, which is usually the entire question. ## Where the output actually surfaces This is where people lose an hour. - **`opa eval`** writes print output to **stderr**. The decision itself — the JSON document — goes to stdout. If your habit is `opa eval ... | jq`, the print lines are still on your terminal but are not in the piped stream, and if you redirect stdout to a file the two are separated. Nothing you print ever appears inside the result JSON. - **`opa test`** hides it by default. Run with **`-v`** to see print output alongside the trace for the tests you are working on. - **`opa run` / the REPL** behaves like `opa eval`: print goes to the process's stderr. - **OPA as a server.** The output belongs to the server process, not to the HTTP response. A caller who POSTs input and reads the decision back sees nothing; you need access to the process's own output stream. That is why print is a local debugging aid, not a way to get visibility out of a running deployment. - **OPA embedded in a host application.** Print statements are erased at compile time unless the embedding application turns them on and provides a hook that decides where the text goes. The consequence is concrete: the same policy that prints happily under `opa eval` on your laptop can print absolutely nothing when the identical bundle is evaluated inside a service, and that is not a bug in your rule. ## `print` versus `trace` Rego also has an older `trace()` built-in, which takes a single string and emits a **Note** event into the evaluation trace rather than writing to stderr. Notes are only visible when you ask for an explanation (`--explain=notes`), and `trace` takes one string, so you end up building the message with `sprintf` first. `print` is the modern replacement: variadic, typed-value friendly, undefined-tolerant, and visible without extra flags. Reach for `trace` only when you specifically want the message interleaved with the rest of an explanation stream. ## Hygiene A print left in a rule that ships is a small, permanent cost: it runs on every evaluation, on every iteration of every collection the body walks, and on a busy admission path that adds up while telling nobody anything useful, because the output goes somewhere no operator is reading. Treat print like a temporary edit — the durable version of the same information belongs in the decision itself, as a message that names the offending resource and the value observed. If you want a print to survive, put it behind a test rather than in the production path: the test run is where `-v` makes it visible on purpose.
- Your rule prints fine under opa eval but nothing appears when the same bundle runs inside a service. What happened?Print statements are erased at compile time unless the embedding application enables them and supplies a print hook, and even when enabled the text goes to that process's own output, never into the decision returned to the caller. Nothing is wrong with the rule; the visibility simply does not exist in that deployment. Debug it locally against the same input instead of trying to make production talk.
- Why can print() take an undefined value when other built-ins cannot?It is a deliberate exception. Ordinary built-ins propagate undefined: an undefined argument makes the call undefined, which makes the whole body undefined. Print instead renders that operand as `<undefined>` and still succeeds, so it can report the absence of a field rather than being silenced by it — which is exactly the case you are usually debugging.
- Is print() a reasonable way to log policy decisions for the security team?No. It writes to the evaluating process's stderr, not into the decision, so it is invisible to the caller and unstructured for anything downstream. Anything the security team needs to see belongs in the decision result itself — a message carrying the resource and the observed value — or in OPA's own decision-logging machinery, which is built for that job.
It is a sticky note on the workbench, not an entry in the shipping manifest: helpful while you have the thing open, invisible to whoever receives the result.
saying these in an interview costs you the question
- Thinks print output appears in the JSON result
- Believes an undefined argument makes print undefined
- Assumes print can make a rule fail or fire
- Uses print as production logging for auditors
- Expects opa test to show print output by default