Files
opentf/docs/diagnostics.md
Diógenes Fernandes 493f44ef76 Fix typos in the diagnostics.md docs (#3306)
Signed-off-by: Diogenes Fernandes <diofeher@gmail.com>
2025-09-25 15:10:14 +01:00

496 lines
25 KiB
Markdown

# OpenTofu Diagnostics Guide
"Diagnostics" is the general term we use to describe the error and warning
messages that OpenTofu returns when there are problems with the configuration,
or when interactions with external systems fail.
This document is an overview of how we typically use diagnostics in OpenTofu.
It includes both some technical information about how we represent diagnostics
in code, and some more subjective information about the writing style we most
often use in diagnostic messages.
## Diagnostics in Code
Diagnostics are modelled using the types from
[the `tfdiags` package](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags).
In particular:
- [`tfdiags.Diagnostics`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics)
represents a set of zero or more diagnostics.
A total lack of diagnostics is usually represented by a `nil` value of this
type.
When constructing sets of diagnostics to return we typically don't worry
about the order they are returned in, even though we return them using a
slice type. The UI-layer code uses
[`tfdiags.Diagnostics.Sort`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics.Sort)
to place all of the collected diagnostics into a predictable order before
rendering them, and so that function effectively turns the set of
diagnostics into an ordered list of diagnostics _just in time_.
- [`tfdiags.Diagnostic`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostic)
is an interface type that all diagnostic values implement.
In practice values of this type are often created automatically as an
implementation detail of [`Diagnostics.Append`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics.Append),
which accepts various types that _don't_ directly implement
`Diagnostic` and then automatically wraps them in a type that does.
In particular:
- We often use [`hcl.Diagnostic`](https://pkg.go.dev/github.com/hashicorp/hcl/v2#Diagnostic)
to describe problems related to the configuration or operations that are
strongly related to parts of the configuration, because it is the most
fully-fledged type of diagnostic we allow including support for source
ranges and relevant expressions as described later.
It's also acceptable to append a whole `hcl.Diagnostics` (the HCL
equivalent of `tfdiags.Diagnostics`) in which case each diagnostic
will be wrapped and appended in turn. This is common when calling
HCL's own functions and passing on its diagnostics verbatim.
- Normal `error` values can be appended to a `tfdiags.Diagnostics`, but
that's mainly for historical reasons -- adapting code that was present
before the diagnostic models were added -- and should not be used in new
code because it typically results in low-quality diagnostics that don't
meet the style guidelines later in this document.
One exception is for "should never happen" cases: we sometimes use
`error` directly in that case to avoid overwhelming the surrounding
code with the construction of a full diagnostic.
Package `tfdiags` also includes some functions for constructing other kinds
of diagnostics, including:
- [`tfdiags.Sourceless`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Sourceless)
is good for diagnostics that don't relate to any part of the configuration,
such as when reporting incorrect usage of a command line argument.
- [`tfdiags.AttributeValue`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#AttributeValue) and
[`tfdiags.WholeContainingBody`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#WholeContainingBody)
produce special "contextual diagnostics" that must be transformed by
calling [`InConfigBody`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics.InConfigBody)
on the resulting `Diagnostics` value. This is a special mechanism used
when the subsystem generating the diagnostic does not have direct access
to the configuration itself, such as when a provider returns a diagnostic
via the provider wire protocol.
- [`tfdiags.Severity`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Severity)
(and its HCL equivalent [`hcl.DiagnosticSeverity`](https://pkg.go.dev/github.com/hashicorp/hcl/v2#DiagnosticSeverity))
are how we distinguish between "error" and "warning" diagnostics.
The `tfdiags.Diagnostics.HasErrors` method returns true if the diagnostics
contains at least one with the severity `tfdiags.Error`.
The most common pattern for handling diagnostics in code is:
1. Declare `var diags tfdiags.Diagnostics` at the very start of a function.
2. During the function's body, whenever calling another function that might
produce its own diagnostics, capture them into a separate variable
(often called `moreDiags`, or `hclDiags` if the return type is
`hcl.Diagnostics`) and then immediately append them to the main `diags`
using `tfdiags.Diagnostics.Append`.
If subsequent code depends on the success of the call, check
`moreDiags.HasErrors()` (or similar) and return early if it returns `true`.
3. If the function generates any diagnostics of its own, append them directly
to `diags`.
4. At all exit points of the function, return `diags` regardless of whether
it has been assigned to or whether it contains errors. This ensures that
we always return any warnings that might have been produced and avoids
the risk of missing certain return paths under future maintenance if we
introduce additional diagnostics later.
Here's a code-example version of the above advice:
```go
func Example() (anything, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
somethingElse, moreDiags := otherFunction()
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
// NOTE: it isn't _always_ necessary to return immediately when there
// are errors, as long as the callee clearly documents what it
// guarantees about an errored result and the caller is able to
// work within those limitations. Collecting multiple errors to
// return together is often desirable.
//
// If the caller cannot continue at all though, or if continuing is
// likely to cause redundant errors that just restate the same problem
// in more confusing terms, then...
return nil, diags
}
if isProblematic(somethingElse) {
// A function might need to generate its own diagnostics if it detects
// a problem directly.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
// ...
})
return nil, diags
}
// ...
// The final return statement should include diags even if no errors
// were detected along the way, because it might contain warnings.
return something, diags
}
```
Some functions diverge from this pattern for special reasons, such as capturing
multiple sets of child function diagnostics and then using some logic to decide
which ones to append, or processing multiple items in a loop and appending
new diagnostics for each iteration. The above is just a general example of the
most common case, not a fixed template to follow in all cases.
## Information in a Diagnostic
The general model of `tfdiags.Diagnostic` has the following parts, though not
all implementations of the interface make use of all of them:
- Severity: either `tfdiags.Error` or `tfdiags.Warning`.
- Description: the main human-readable text describing the problem. This
has the following fields:
- Summary: A short, terse description of the general type of problem
that has occurred.
- Detail: A longer description of the problem, sometimes including multiple
paragraphs of information.
- Address: The address of some object that the error relates to, which
is most often a resource instance address.
OpenTofu does not currently have a localized UI, so built-in diagnostics
always have their summary and detail written in US English. There's more
subjective guidance about the content of these fields in sections below.
- Source location information: optional references to parts of the configuration
that the problem relates to. This has the following fields:
- Subject: source range for the part of the configuration that caused the
problem or that the problem is directly about.
- Context: optional source range of a larger section of configuration that
might make the cause of the problem easier to quickly understand if
included in the diagnostic message. The Context source range must always
contain the Subject source range within it.
The UI uses the context and subject together to display a source code
snippet. The lines of code included in the snippet cover both the context
and the subject, and then the subject itself is rendered with an underline
if we're rendering into a terminal that supports that style.
We don't use "context" very often, but it can be useful if the problem
we're describing is that just one part of a larger source element is
problematic. For example, if one of the operands to the `+` operator
isn't a number then that operand would be the "subject" but the entire
addition operation could be returned as "context", so that both of the
operands and the `+` symbol will definitely be included in the rendered
diagnostic too.
- Expression-related information: optional information about an expression whose
evaluation cause the problem. This has the following fields:
- Expression: The `hcl.Expression` representing the expression itself.
- EvalContext: The `hcl.EvalContext` that the expression was being evaluated
in.
The diagnostic renderer for the UI uses this information, when available,
to offer some extra hints about the values of any symbols that were used
in the expression, because it's often the dynamic values that cause a
problem, rather than the syntax used to obtain them.
- Extra info: this is a rather underspecified collection of assorted other
information that's only relevant in very specific contexts. Refer to the
`tfdiags` package documentation for more information.
There's _some_ guidance on this later in this document, but it's focused
only on a few main cases.
## Diagnostic Description Writing Style
Although there is some variation in diagnostic writing style, particularly in
parts of the system like state storage backends which were originally written by
third-parties, most of the _built-in_ diagnostics follow a relatively consistent
writing style that is in turn based on the writing style used by HCL itself in
its own diagnostics, because HCL and OpenTofu diagnostics often mix together
in the same set of problems.
The "summary" should typically be a very short and concise description of
what was wrong and what was wrong about it. Our summaries typically don't
include any user-chosen information such as symbol names, because that means a
particular kind of problem is always described using the same text and so
readers can become familiar enough with the summaries of problems they see
frequently to skip reading the rest of the diagnostic when skimming.
The following are some real examples of summaries currently used across both
HCL and OpenTofu:
- Unsupported operator
- Duplicate argument
- Invalid index
- Unexpected end of template
- Invalid template interpolation value
- Invalid default value for variable
- Required variable not set
- Invalid "count" attribute
The "detail" text is where we tend to put most of the information, and so
there's a lot more variation here but ideally a good diagnostic detail
should mention the following information, usually in the following order:
- What was wrong and what was wrong about it: similar to the summary but this
time including information about specifically what was wrong, such as the
name of the input variable whose default value was invalid.
- Why the situation is problematic, if knowing that relies on some
characteristic of OpenTofu's design that might not be obvious to a newcomer.
- What should be done to fix it, or (if it's unclear what the author's intention
was) a question-sentence that implies a _possible_ solution, often starting
with the words "Did you mean" and ending with a question mark.
While the summary message is often terse and uses only minimal punctuation,
the detail message should always be written in full sentences including
end-of-sentence punctuation (`.`, `?`). If "what was wrong about it" is
coming from the string representation of an `error` value, we typically
present it with a prefix ending with a colon and then append a period `.`
after the error string, and format the error itself using `tfdiags.FormatError`,
like this:
```go
Detail: fmt.Sprintf("Unsuitable value for thingy: %s.", tfdiags.FormatError(err))
```
If the second and third items in the above take more than a few words, it's
helpful to split them into their own paragraphs for easier scanning. When
writing multiple paragraphs in a detail message they should be separated by
`\n\n` -- two newline characters.
In many cases our diagnostics only include a subset of this information because
either the reason why it's problematic is relatively clear or because we don't
have any specific suggestion for how to solve the problem, but the following
is an example of a real diagnostic message from OpenTofu at the time of writing
this documentation which includes all of these parts:
```
Error: Invalid for_each argument
The "for_each" map includes keys derived from resource attributes that cannot
be determined until apply, and so OpenTofu cannot determine the full set of keys
that will identify the instances of this resource.
When working with unknown values in for_each, it's better to define the map keys
statically in your configuration and place apply-time results only in the map
values.
Alternatively, you could use the planning option -exclude=aws_instance.example
to first apply without this object, and then apply normally to converge.
```
The text immediately after "Error:" above is the summary for this diagnostic.
The paragraphs that follow are all a single "detail" string.
That was a particularly extreme diagnostic message with lots of information to
communicate. Most diagnostics are not so complicated; the following is an
example with less information to communicate:
```
Error: Invalid value for input variable
The given value is not suitable for var.example declared
at example.tf:12,1: a string is required.
```
This example also illustrates a situation where there are two different source
locations that could be relevant: the input variable's declaration or the
expression that's used to define its value. Because this message is talking
about a problem with the _value_, the diagnostic should have the source
"Subject" set to the expression that defined it, but it also mentions the
location of the declaration as part of the detail text as some additional
context.
Some other notes about some other specific situations that arise sometimes:
- If a diagnostic message includes a suggestion for a shell command to run
or a URL to visit for more information, use a paragraph that ends with a
colon, followed by a single newline, four spaces for indentation, and then the
command or URL:
```
To view the root module output values, run:
tofu output
```
The goal of this formatting is to make it very clear what part of the
message is intended to be copied and used elsewhere, by placing it on a
line of its own without any surrounding punctuation. The indented text
should ideally be formatted so that the user can copy it _verbatim_ into
whatever place it will be used.
The diagnostic renderer also has a special case where it will not try to
word-wrap a line that begins with spaces, and so this layout has the
useful side-effect of avoiding introducing extra newline characters into
a command line that is intended to be copied.
- There are some terminology choices we use to refer to some OpenTofu-specific
ideas and concepts that disagree slightly with terminology used in the code.
These differences are the result of learning from feedback from folks who
had been confused by the original terminology, even though the code still
often uses the original terminology:
- Instead of referring to "unknown values" or "computed values" we say that
values are "known after apply" or "cannot be determined until apply".
- In HCL the word "variable" means anything that's available to refer to
in the current evaluation context, which is confusing because OpenTofu
itself uses that word to refer only to input variables.
Sometimes messages are generated by HCL itself and so it's unavoidably
confusing, but when we're generating messages _inside OpenTofu_ we
use the two words "input variable" to refer to an input variable,
and "symbol" or "object" (depending on whether we're talking about
the name itself or what the name refers to) as the general word for
something you can refer to in an expression.
- For consistency with our use of "input variable" to distinguish from
HCL's more general meaning of "variable", we also tend to write
"local value" and "output value" when referring to those concepts, rather
than using the shorthands "locals" and "outputs".
- HCL distinguishes between "attributes" meaning the named keys inside an
object type, and "arguments" meaning the names used for individual
settings inside a configuration block.
OpenTofu itself uses those words a little more interchangeably because
in _many_ cases the configuration arguments in a block directly
correspond to the attributes of an object created by evaluating that
block.
However, if a particular error message is talking about a configuration
setting inside a block it's better to use "argument" rather than
"attribute" because that's then consistent with error messages that
HCL itself might generate.
Go uses the term "field" to describe an element of a struct type, and
JavaScript and JSON use the word "property" to describe an element of
an object type. We don't use either of those words in OpenTofu: the
elements of an object are its _attributes_, and the settings available
in a configuration block are its _arguments_. The string values that
identify elements of a map are called "keys".
- The `cty` terminology "marks" or "value marks" refers to an implementation
detail that should never be mentioned directly in an error message.
Instead, we use specific terminology related to what each mark type
is representing: "sensitive values", "ephemeral values", etc.
- `aws_instance` is an example of a "resource _type_", not of a "resource",
even though the provider protocol uses the single noun "resource" to refer
to both ideas.
A "resource" is what's declared by a `resource`, `data`, or `ephemeral`
block. A "resource _instance_" is what such a block can declare zero
or more of, when using the `count`, `for_each`, or `enabled` arguments.
- Although there are certainly some historical diagnostic messages that
predate this adjustment of terminology, new error messages should use
"managed resource" to refer to the kind of resource that's declared
using a `resource` block, "data resource" for `data` blocks, and
"ephemeral resource" for an `ephemeral` block.
In the code we refer to these three as "resource _modes_", but that is
internal terminology that should never appear in a diagnostic message.
- When a file or directory path appears as part of a diagnostic message, it
should typically be presented relative to the current working directory and
should use the syntax conventions of the platform where OpenTofu is running.
In particular, we return paths using backslashes as the separator when we
are running on Windows, but normal slashes otherwise. Using the Go
`filepath` package is a good way to get this right, though you might need
to add some complexity to your tests to make them pass on all platforms.
- If an error message is describing a "should never happen" case, we typically
end the detail string with the sentence "This is a bug in OpenTofu.". This
hopefully prompts the reader that this wasn't directly caused by something
they did, and so they should probably open a bug report in the
OpenTofu repository instead of just trying to solve it themselves.
For this kind of error message we often relax our preference against
mentioning implementation details in the error message, because the most
likely next step is for the user to copy-paste the entire message into their
bug report text and so the final reader of the message is OpenTofu
maintainers rather than OpenTofu users.
For example, it can be okay to use internal terminology like "cty marks" and
use the `GoString` representations of values in a "This is a bug in
OpenTofu" detail message, if that's the most concise way to capture the
information the OpenTofu maintainers would need to debug the problem.
## Diagnostics caused by unknown or sensitive values
When a diagnostic has expression information associated with it, the diagnostic
renderer for the UI includes some additional information about the values
that were in scope, like this:
```
var.greeting is "Hello"
var.items is list of string with 5 elements
```
By default, this renderer will not mention any symbol which refers to an unknown
or sensitive value. That was not historically true: originally, this could
say something like "var.example is a string, known only after apply".
Those who are less familiar with these concepts often misunderstood the
"known only after apply" part of the message as being _the problem itself_,
rather than just context to help diagnose the problem, and so the UI no longer
mentions "unknown-ness" or "sensitive-ness" in most cases.
However, there are some diagnostics messages that _are_ directly caused by the
presence of an unknown or sensitive value, in which case it's helpful to
mention that in the summary of values that were in scope.
To allow for this, we set the "extra info" field of a diagnostic to contain
an implementation of one of the following interfaces:
- [`tfdiags.DiagnosticExtraBecauseUnknown`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#DiagnosticExtraBecauseUnknown)
for a problem that's caused by an unknown value.
(Remember that the _text_ of the error message should refer to this as "known
only after apply", or similar.)
- [`tfdiags.DiagnosticExtraBecauseSensitive`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#DiagnosticExtraBecauseSensitive)
for situations where a sensitive value was used in a location that OpenTofu
cannot permit it, such as in the instance key of a resource instance.
These extra markers should be used only when mentioning the unknown or sensitive
values in the diagnostic message is likely to help with debugging a problem.
If the problem is not directly caused by unknown or sensitive values then
neither of these should be used, to avoid creating a distracting
[red herring](https://en.wikipedia.org/wiki/Red_herring) for the reader.
## Consolidation of Diagnostics
The UI layer has some special rules for finding sets of similar diagnostics
and showing them as just a single diagnostic referring to the first example
of a problem, with a short extra note about how many other similar diagnostics
there are.
```
(and 2 similar warnings elsewhere)
```
The main implementation of this behavior is in
[`tfdiags.Diagnostics.Consolidate`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Diagnostics.Consolidate),
but we allow end-users to customize (using command line options) whether this
consolidation applies to errors or warnings separately. By default, we
consolidate only warnings.
For a severity that is subject to consolidation, the main behavior is to group
together diagnostics that have the same "summary" text, and this is part of
why we tend to use terse, fixed strings in the summary field.
There are two extra mechanisms for customizing this behavior for specific
diagnostic messages:
- If the "extra info" of a diagnostic contains an implementation of
[`tfdiags.DiagnosticExtraDoNotConsolidate`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#DiagnosticExtraDoNotConsolidate)
then that diagnostic is not eligible for consolidation at all, regardless
of how similar it might be to other diagnostics in the same set.
- If the "extra info" of a diagnostic contains an implementation of
[`tfdiags.Keyable`](https://pkg.go.dev/github.com/opentofu/opentofu/internal/tfdiags#Keyable)
then the string returned by its `ExtraInfoKey` method is used _in addition to_
the summary text for deciding what to consolidate.
For example, if there were three warnings with the same summary text but
two of them have the same `ExtraInfoKey` and the third has a different
one then only the first two would be able to consolidate.
The `ExtraInfoKey` is an internal key used for comparison only and is never
exposed in the UI, so it can be set to whatever makes sense to define
separate consolidation groups for diagnostics with a specific summary.