Skip to content

Commit eefc63b

Browse files
committed
chore(error): anchor templates on Google RPC
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent 41f48a0 commit eefc63b

8 files changed

Lines changed: 170 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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
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.
8+
9+
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.
10+
11+
## Mapping
12+
13+
| Trogon template field | Google RPC target |
14+
|-----------------------|-------------------|
15+
| `code` | `google.rpc.Status.code` |
16+
| `message` | `google.rpc.Status.message` |
17+
| `domain` | `google.rpc.ErrorInfo.domain` |
18+
| `reason` | `google.rpc.ErrorInfo.reason` |
19+
| `metadata` | `google.rpc.ErrorInfo.metadata` |
20+
| `help_links` | `google.rpc.Help.links` |
21+
22+
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.
23+
24+
## Boundaries
25+
26+
Trogon error templates do not declare which RPC can return an error. That belongs in a method-level outcome option.
27+
28+
Trogon error templates also do not model successful outcomes. `trogon.error.v1alpha1.Code` intentionally mirrors only Google RPC error codes and omits `OK`.
29+
30+
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.
31+
32+
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.
33+
34+
## Example
35+
36+
A template like this:
37+
38+
```proto
39+
message UserNotFoundError {
40+
option (trogon.error.v1alpha1.message).template = {
41+
domain: "identity.trogonstack.dev"
42+
reason: "USER_NOT_FOUND"
43+
message: "The requested user was not found."
44+
code: NOT_FOUND
45+
visibility: VISIBILITY_PUBLIC
46+
};
47+
48+
string user_id = 1 [(trogon.error.v1alpha1.field).visibility = VISIBILITY_PUBLIC];
49+
}
50+
```
51+
52+
describes this Google RPC detail identity:
53+
54+
```json
55+
{
56+
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
57+
"domain": "identity.trogonstack.dev",
58+
"reason": "USER_NOT_FOUND",
59+
"metadata": {
60+
"userId": "usr_123"
61+
}
62+
}
63+
```
64+
65+
The surrounding transport error still comes from the runtime protocol. Trogon only defines the stable error detail contract.
66+
67+
## References
68+
69+
- [Document Errors in Proto](../how-to/document-errors-in-proto.md)
70+
- [Google RPC Status](https://cloud.google.com/tasks/docs/reference/rpc/google.rpc#status)
71+
- [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: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
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+
57
## Example
68

79
```proto
@@ -20,13 +22,12 @@ message ResourceAvailabilityError {
2022
{url: "https://docs.acme.com/compute", description: "Compute Docs"}
2123
]
2224
metadata: [
23-
{key: "component", value: "compute", visibility: VISIBILITY_PUBLIC},
24-
{key: "team", value: "platform-compute", visibility: VISIBILITY_INTERNAL}
25+
{key: "component", value: "compute", visibility: VISIBILITY_PUBLIC}
2526
]
2627
};
2728
28-
string zone = 1;
29-
string vm_type = 2;
29+
string zone = 1 [(trogon.error.v1alpha1.field).visibility = VISIBILITY_PUBLIC];
30+
string vm_type = 2 [(trogon.error.v1alpha1.field).visibility = VISIBILITY_PUBLIC];
3031
string service = 3 [(trogon.error.v1alpha1.field) = {
3132
visibility: VISIBILITY_PUBLIC,
3233
default_value: "compute-api"
@@ -38,18 +39,70 @@ message ResourceAvailabilityError {
3839
}
3940
```
4041

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

4390
- `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.
91+
- 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.
92+
- `domain` and `reason` describe the `google.rpc.ErrorInfo` identity for this error. `reason` must be stable within the `domain`.
93+
- `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.
4694
- 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`).
95+
- `visibility` controls who sees the field at runtime. `VISIBILITY_UNSPECIFIED` is invalid for emitted metadata.
4896
- `value_policy.default_value` substitutes a value when the runtime field is empty.
4997
- `value_policy.value` pins the field to a contract-fixed value; the runtime payload is ignored on emit.
98+
- Payload fields use their protobuf JSON names as `ErrorInfo.metadata` keys. For example, `vm_type` emits `vmType`.
99+
- Shared protos should use only `VISIBILITY_PUBLIC` and `VISIBILITY_PRIVATE`. Internal-only metadata belongs in runtime enrichment or internal overlays, not shared descriptors.
100+
- `visibility` is a Trogon filtering policy. It is not part of `google.rpc.ErrorInfo`.
50101

51102
## References
52103

53104
- [../../proto/trogon/error/v1alpha1/code.proto](../../proto/trogon/error/v1alpha1/code.proto)
54105
- [../../proto/trogon/error/v1alpha1/options.proto](../../proto/trogon/error/v1alpha1/options.proto)
55106
- [../../proto/trogon/error/v1alpha1/visibility.proto](../../proto/trogon/error/v1alpha1/visibility.proto)
107+
- [Google RPC Status](https://cloud.google.com/tasks/docs/reference/rpc/google.rpc#status)
108+
- [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)