Skip to content

Commit 53ac7ec

Browse files
authored
fix(error): align templates with Error Specification ADR (#48)
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent 41f48a0 commit 53ac7ec

8 files changed

Lines changed: 180 additions & 39 deletions

File tree

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Documentation
22

33
- [Document Errors in Proto](how-to/document-errors-in-proto.md)
4+
- [Google RPC Error Templates](explanation/google-rpc-error-template.md)
45
- [Protobuf Extension Naming](explanation/protobuf-extension-naming.md)
56
- [Consistency Pattern](explanation/consistency-pattern.md)

docs/explanation/consistency-pattern.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,12 @@ When projection fails to meet consistency requirements, return appropriate gRPC
121121
**Projection Timeout** (timeout exceeded):
122122
- Status Code: `UNAVAILABLE` (503)
123123
- Use `google.rpc.ErrorInfo` for structured error details
124-
- Suggested metadata: min_version, current_version, attempts, elapsed_ms
124+
- Suggested metadata: `minVersion`, `currentVersion`, `attempts`, `elapsedMs`
125125

126126
**Snapshot Expired** (ExactVersion only, version moved past):
127127
- Status Code: `FAILED_PRECONDITION` (400)
128128
- Use `google.rpc.ErrorInfo` for structured error details
129-
- Suggested metadata: requested_version, current_version
129+
- Suggested metadata: `requestedVersion`, `currentVersion`
130130

131131
Consider using `google.rpc.Status` with `google.rpc.ErrorInfo` for rich error responses that clients can programmatically handle.
132132

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Google RPC Error Templates
2+
3+
Trogon error options describe the Google RPC rich error details a runtime should emit. They are descriptor metadata, not a replacement for gRPC, Connect, or HTTP error envelopes.
4+
5+
## Why This Exists
6+
7+
This package is based on [ADR 0129349218: Error Specification](https://straw-hat-team.github.io/adr/adrs/0129349218/README.html). The ADR defines the broader error model: stable error identity, structured metadata, visibility-aware filtering, help links, retry guidance, and boundary processing.
8+
9+
`trogon.error.v1alpha1` is the shared-protobuf projection of that model. It keeps the pieces that can safely live in published descriptors and maps them to Google RPC rich error details.
10+
11+
Without a shared proto annotation, every runtime has to construct `google.rpc.Status`, `google.rpc.ErrorInfo`, and related details by hand. That makes stable fields such as `domain`, `reason`, and metadata keys easy to drift across languages.
12+
13+
The `trogon.error.v1alpha1` package keeps that contract close to the typed error payload message. Code generators and runtime adapters can read the same descriptor metadata and produce consistent Google RPC details.
14+
15+
## Mapping
16+
17+
| Trogon template field | Google RPC target |
18+
|-----------------------|-------------------|
19+
| `code` | `google.rpc.Status.code` |
20+
| `message` | `google.rpc.Status.message` |
21+
| `domain` | `google.rpc.ErrorInfo.domain` |
22+
| `reason` | `google.rpc.ErrorInfo.reason` |
23+
| `metadata` | `google.rpc.ErrorInfo.metadata` |
24+
| `help_links` | `google.rpc.Help.links` |
25+
26+
Payload fields annotated with `trogon.error.v1alpha1.field` supply emission-specific `ErrorInfo.metadata` values. Template metadata supplies fixed metadata values that apply to every emission. Metadata keys should use lowerCamelCase; payload fields use their protobuf JSON names.
27+
28+
## Boundaries
29+
30+
Trogon error templates do not declare which RPC can return an error. That belongs in a method-level outcome option.
31+
32+
Trogon error templates also do not model successful outcomes. `trogon.error.v1alpha1.Code` intentionally mirrors only Google RPC error codes and omits `OK`.
33+
34+
Shared error protos should expose only public or private metadata. Internal-only metadata belongs in runtime enrichment, observability pipelines, or internal-only overlays because descriptor annotations are visible to anyone who receives the proto.
35+
36+
This is the intentional difference from the full ADR model. The ADR still describes internal runtime errors and internal metadata filtering. Shared protos do not name that visibility because naming an internal descriptor value invites accidental publication.
37+
38+
The runtime owns protocol adaptation. A gRPC runtime can emit `google.rpc.Status` with typed details. A Connect runtime can expose the same semantics through Connect errors and strongly typed details.
39+
40+
## Example
41+
42+
A template like this:
43+
44+
```proto
45+
message UserNotFoundError {
46+
option (trogon.error.v1alpha1.message).template = {
47+
domain: "identity.trogonstack.dev"
48+
reason: "USER_NOT_FOUND"
49+
message: "The requested user was not found."
50+
code: NOT_FOUND
51+
visibility: VISIBILITY_PUBLIC
52+
};
53+
54+
string user_id = 1 [(trogon.error.v1alpha1.field).visibility = VISIBILITY_PUBLIC];
55+
}
56+
```
57+
58+
describes this Google RPC detail identity:
59+
60+
```json
61+
{
62+
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
63+
"domain": "identity.trogonstack.dev",
64+
"reason": "USER_NOT_FOUND",
65+
"metadata": {
66+
"userId": "usr_123"
67+
}
68+
}
69+
```
70+
71+
The surrounding transport error still comes from the runtime protocol. Trogon only defines the stable error detail contract.
72+
73+
## References
74+
75+
- [ADR 0129349218: Error Specification](https://straw-hat-team.github.io/adr/adrs/0129349218/README.html)
76+
- [Document Errors in Proto](../how-to/document-errors-in-proto.md)
77+
- [Google RPC Status](https://cloud.google.com/tasks/docs/reference/rpc/google.rpc#status)
78+
- [Google RPC ErrorInfo](https://cloud.google.com/spanner/docs/reference/rpc/google.rpc#errorinfo)

docs/explanation/protobuf-extension-naming.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ trogon/stream/v1alpha1/options.proto
108108
109109
trogon/error/v1alpha1/options.proto
110110
├─ MessageOptions { template } → message (870012)
111-
│ └─ Template (nested) { domain, reason, message, code }
111+
│ └─ Template (nested) { domain, reason, message, code, help_links, metadata }
112112
└─ FieldOptions { visibility } → field (870013)
113113
```
114114

docs/how-to/document-errors-in-proto.md

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
This guide shows the intended protobuf shape for typed error payload messages that use `trogon.error.v1alpha1`.
44

5+
Trogon error annotations are descriptor-time templates for the Google RPC rich error model. They describe the `google.rpc.Status` and `google.rpc.ErrorInfo` details a runtime should emit when the typed error occurs.
6+
7+
This is the protobuf projection of [ADR 0129349218: Error Specification](https://straw-hat-team.github.io/adr/adrs/0129349218/README.html). The ADR explains the full boundary model; this guide only documents the descriptor fields that are safe to share.
8+
59
## Example
610

711
```proto
@@ -20,13 +24,12 @@ message ResourceAvailabilityError {
2024
{url: "https://docs.acme.com/compute", description: "Compute Docs"}
2125
]
2226
metadata: [
23-
{key: "component", value: "compute", visibility: VISIBILITY_PUBLIC},
24-
{key: "team", value: "platform-compute", visibility: VISIBILITY_INTERNAL}
27+
{key: "component", value: "compute", visibility: VISIBILITY_PUBLIC}
2528
]
2629
};
2730
28-
string zone = 1;
29-
string vm_type = 2;
31+
string zone = 1 [(trogon.error.v1alpha1.field).visibility = VISIBILITY_PUBLIC];
32+
string vm_type = 2 [(trogon.error.v1alpha1.field).visibility = VISIBILITY_PUBLIC];
3033
string service = 3 [(trogon.error.v1alpha1.field) = {
3134
visibility: VISIBILITY_PUBLIC,
3235
default_value: "compute-api"
@@ -38,18 +41,71 @@ message ResourceAvailabilityError {
3841
}
3942
```
4043

44+
## Runtime Mapping
45+
46+
When a runtime emits this error, it should build the protocol-native error envelope from the template:
47+
48+
| Trogon option | Google RPC field |
49+
|---------------|------------------|
50+
| `code` | `google.rpc.Status.code` |
51+
| `message` | `google.rpc.Status.message` |
52+
| `domain` | `google.rpc.ErrorInfo.domain` |
53+
| `reason` | `google.rpc.ErrorInfo.reason` |
54+
| `metadata` | `google.rpc.ErrorInfo.metadata` |
55+
| Payload fields | `google.rpc.ErrorInfo.metadata` using proto JSON names |
56+
| `help_links` | `google.rpc.Help.links` |
57+
58+
For the example above, a runtime could emit this JSON representation of a `google.rpc.Status`:
59+
60+
```json
61+
{
62+
"code": 8,
63+
"message": "Requested resources are unavailable.",
64+
"details": [
65+
{
66+
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
67+
"reason": "RESOURCE_AVAILABILITY",
68+
"domain": "compute.googleapis.com",
69+
"metadata": {
70+
"component": "compute",
71+
"region": "us-east-1",
72+
"service": "compute-api",
73+
"vmType": "n2-standard-16",
74+
"zone": "us-east1-b"
75+
}
76+
},
77+
{
78+
"@type": "type.googleapis.com/google.rpc.Help",
79+
"links": [
80+
{
81+
"description": "Compute Docs",
82+
"url": "https://docs.acme.com/compute"
83+
}
84+
]
85+
}
86+
]
87+
}
88+
```
89+
4190
## Notes
4291

4392
- `template` is a message field, so it carries proto3 presence by default.
44-
- The template captures the parts of the error contract that never vary at runtime: `domain`, `reason`, `code`, default `message`, error-level `visibility`, `help_links`, and fixed `metadata` entries.
45-
- `metadata` declares contract-level constants (component, team, subsystem) that attach to every emission without occupying a wire field. Keys must be unique within the template and must not collide with field names on the payload message.
93+
- The template captures the parts of the Google RPC error contract that never vary at runtime: `domain`, `reason`, `code`, default `message`, error-level `visibility`, `help_links`, and fixed `metadata` entries.
94+
- `domain` and `reason` describe the `google.rpc.ErrorInfo` identity for this error. `reason` must be stable within the `domain`.
95+
- `metadata` declares contract-level constants (component, product, support category) that attach to every emission without occupying a wire field. Keys should use lowerCamelCase, must be unique within the template, and must not collide with field names on the payload message.
4696
- Payload fields hold the dynamic context for a single emission. Each field can carry `FieldOptions`:
47-
- `visibility` controls who sees the field at runtime (defaults to `INTERNAL`).
97+
- `visibility` controls who sees the field at runtime. `VISIBILITY_UNSPECIFIED` is invalid for emitted metadata.
4898
- `value_policy.default_value` substitutes a value when the runtime field is empty.
4999
- `value_policy.value` pins the field to a contract-fixed value; the runtime payload is ignored on emit.
100+
- Payload fields use their protobuf JSON names as `ErrorInfo.metadata` keys. For example, `vm_type` emits `vmType`.
101+
- Shared protos should use only `VISIBILITY_PUBLIC` and `VISIBILITY_PRIVATE`. Internal-only metadata belongs in runtime enrichment or internal overlays, not shared descriptors.
102+
- `visibility` is a Trogon filtering policy. It is not part of `google.rpc.ErrorInfo`.
50103

51104
## References
52105

106+
- [ADR 0129349218: Error Specification](https://straw-hat-team.github.io/adr/adrs/0129349218/README.html)
53107
- [../../proto/trogon/error/v1alpha1/code.proto](../../proto/trogon/error/v1alpha1/code.proto)
54108
- [../../proto/trogon/error/v1alpha1/options.proto](../../proto/trogon/error/v1alpha1/options.proto)
55109
- [../../proto/trogon/error/v1alpha1/visibility.proto](../../proto/trogon/error/v1alpha1/visibility.proto)
110+
- [Google RPC Status](https://cloud.google.com/tasks/docs/reference/rpc/google.rpc#status)
111+
- [Google RPC ErrorInfo](https://cloud.google.com/spanner/docs/reference/rpc/google.rpc#errorinfo)

proto/trogon/consistency/v1alpha1/consistency.proto

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ message Consistency {
102102
// - Maximum: 5s (values above should be clamped and logged)
103103
//
104104
// If projection doesn't reach required version within timeout, query should return
105-
// unavailable error with appropriate error metadata (e.g., projection_timeout).
105+
// unavailable error with appropriate error metadata (e.g., projectionTimeout).
106106
optional google.protobuf.Duration timeout_duration = 3;
107107

108108
// Delay between retry attempts.

proto/trogon/error/v1alpha1/options.proto

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,44 @@ import "trogon/error/v1alpha1/visibility.proto";
99
option (elixirpb.file).module_prefix = "TrogonProto.Error.V1Alpha1";
1010

1111
// MessageOptions defines message-level options for error payload messages.
12+
//
13+
// These annotations describe the Google RPC rich error details a runtime
14+
// should emit for this payload. They are not a transport envelope; runtimes
15+
// adapt the template into google.rpc.Status, google.rpc.ErrorInfo, google.rpc.Help,
16+
// or equivalent protocol-native details.
1217
message MessageOptions {
1318
// Template defines the static error template for a message that can be
14-
// adapted into a runtime error representation.
19+
// adapted into a runtime Google RPC error representation.
1520
//
1621
// These fields are intentionally language-neutral so both Elixir and Go
1722
// runtimes can derive their native error template APIs from the same proto
1823
// annotation.
1924
message Template {
20-
// domain identifies the logical owner of the error contract.
25+
// domain maps to google.rpc.ErrorInfo.domain.
2126
// Example: "compute.googleapis.com"
2227
string domain = 1;
2328

24-
// reason identifies the stable machine-readable error reason.
29+
// reason maps to google.rpc.ErrorInfo.reason.
2530
// Example: "RESOURCE_AVAILABILITY"
2631
string reason = 2;
2732

28-
// message is the default human-readable message template.
33+
// message maps to google.rpc.Status.message.
2934
string message = 3;
3035

31-
// code is the canonical error code for the template.
36+
// code maps to google.rpc.Status.code.
3237
Code code = 4;
3338

3439
// visibility controls who can see this error at runtime.
35-
// When unset, defaults to INTERNAL for safety.
40+
// UNSPECIFIED is invalid for emitted error details.
3641
Visibility visibility = 5;
3742

38-
// help_links provides documentation or support links for this error.
43+
// help_links maps to google.rpc.Help links.
3944
repeated HelpLink help_links = 6;
4045

4146
// metadata declares fixed key/value pairs attached to every emission
42-
// of this error. Unlike struct fields with FieldOptions.value, these
43-
// entries have no wire representation on the payload message — they
47+
// of this error. Runtimes copy these entries into
48+
// google.rpc.ErrorInfo.metadata. Unlike struct fields with FieldOptions.value,
49+
// these entries have no wire representation on the payload message — they
4450
// are part of the error contract itself.
4551
//
4652
// Keys must be unique within a template and must not collide with
@@ -61,8 +67,8 @@ message MessageOptions {
6167

6268
// MetadataEntry declares a fixed metadata pair on a Template.
6369
message MetadataEntry {
64-
// key is the metadata key. Lowercase snake_case, matching the
65-
// convention used for FieldOptions-derived metadata keys.
70+
// key is the metadata key. Use lowerCamelCase to match
71+
// google.rpc.ErrorInfo.metadata guidance.
6672
string key = 1;
6773

6874
// value is the literal value attached at every emission. Template
@@ -72,7 +78,7 @@ message MessageOptions {
7278
string value = 2;
7379

7480
// visibility controls who can see this metadata at runtime.
75-
// When unset, defaults to INTERNAL for safety.
81+
// UNSPECIFIED is invalid for emitted error details.
7682
Visibility visibility = 3;
7783
}
7884

@@ -83,10 +89,13 @@ message MessageOptions {
8389
}
8490

8591
// FieldOptions defines field-level options for error payload message fields.
92+
//
93+
// Runtimes copy payload fields into google.rpc.ErrorInfo.metadata unless a
94+
// value policy supplies a default or fixed value.
8695
message FieldOptions {
8796
// visibility controls who can see this metadata field at runtime.
8897
//
89-
// When unset, defaults to INTERNAL for safety.
98+
// UNSPECIFIED is invalid for emitted error details.
9099
Visibility visibility = 1;
91100

92101
oneof value_policy {
@@ -119,13 +128,12 @@ extend google.protobuf.MessageOptions {
119128
// visibility: VISIBILITY_PUBLIC,
120129
// help_links: [{url: "https://docs.acme.com/compute", description: "Compute Docs"}],
121130
// metadata: [
122-
// {key: "component", value: "compute", visibility: VISIBILITY_PUBLIC},
123-
// {key: "team", value: "platform-compute", visibility: VISIBILITY_INTERNAL}
131+
// {key: "component", value: "compute", visibility: VISIBILITY_PUBLIC}
124132
// ]
125133
// };
126134
//
127-
// string zone = 1;
128-
// string vm_type = 2;
135+
// string zone = 1 [(trogon.error.v1alpha1.field).visibility = VISIBILITY_PUBLIC];
136+
// string vm_type = 2 [(trogon.error.v1alpha1.field).visibility = VISIBILITY_PUBLIC];
129137
// string service = 3 [(trogon.error.v1alpha1.field) = {
130138
// visibility: VISIBILITY_PUBLIC,
131139
// default_value: "compute-api"
@@ -153,11 +161,11 @@ extend google.protobuf.FieldOptions {
153161
// domain: "identity.acme.com",
154162
// reason: "USER_NOT_FOUND",
155163
// message: "The requested user was not found.",
156-
// code: NOT_FOUND
164+
// code: NOT_FOUND,
165+
// visibility: VISIBILITY_PUBLIC
157166
// };
158167
//
159168
// string user_id = 1 [(trogon.error.v1alpha1.field).visibility = VISIBILITY_PUBLIC];
160-
// string internal_trace = 2; // defaults to INTERNAL
161169
// }
162170
optional FieldOptions field = 870013;
163171
}

proto/trogon/error/v1alpha1/visibility.proto

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,20 @@ option (elixirpb.file).module_prefix = "TrogonProto.Error.V1Alpha1";
77

88
// Visibility controls who can see a given error metadata field.
99
//
10-
// This aligns with the three-tier model in the Trogon Error runtime
11-
// (Trogon.Error.MetadataValue.visibility/0).
10+
// Visibility is an exposure contract for shared descriptors. It is not a
11+
// secrecy boundary for data encoded in proto annotations: anyone with the
12+
// descriptor can read those keys and values. Internal-only metadata belongs in
13+
// runtime enrichment, observability pipelines, or internal-only overlays.
1214
//
13-
// Default (unspecified) is treated as INTERNAL for safety.
15+
// Code generators should reject UNSPECIFIED for emitted error details.
1416
enum Visibility {
1517
VISIBILITY_UNSPECIFIED = 0;
1618

17-
// INTERNAL fields are only visible to internal systems and developers.
18-
// Use for stack traces, debug identifiers, internal correlation IDs.
19-
VISIBILITY_INTERNAL = 1;
20-
2119
// PRIVATE fields are visible to authenticated users but not the general public.
2220
// Use for account-specific context that the caller owns.
23-
VISIBILITY_PRIVATE = 2;
21+
VISIBILITY_PRIVATE = 1;
2422

2523
// PUBLIC fields are visible to all users including end consumers.
2624
// Use for identifiers the caller already knows or non-sensitive context.
27-
VISIBILITY_PUBLIC = 3;
25+
VISIBILITY_PUBLIC = 2;
2826
}

0 commit comments

Comments
 (0)