| # 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. |