Skip to content

Commit a03e524

Browse files
josecolellaclaude
andauthored
feat: add OTel-compatible telemetry utility (#240)
Signed-off-by: Jose Colella <jose.colella@gusto.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 81602aa commit a03e524

5 files changed

Lines changed: 1000 additions & 0 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Telemetry Utility Design
2+
3+
## Overview
4+
5+
Add an OTel-compatible telemetry utility to the OpenFeature Ruby SDK that creates
6+
structured evaluation events from hook context and evaluation details. This utility
7+
is dependency-free (no OTel gem required) and follows the pattern established by
8+
the Go SDK's `telemetry` package.
9+
10+
Addresses: https://github.com/open-feature/ruby-sdk/issues/176
11+
12+
## References
13+
14+
- [OpenFeature Spec Appendix D (Observability)](https://openfeature.dev/specification/appendix-d/)
15+
- [OTel Semantic Conventions for Feature Flags](https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/)
16+
- [Go SDK telemetry package](https://github.com/open-feature/go-sdk/tree/main/openfeature/telemetry)
17+
- [JS SDK reference PR](https://github.com/open-feature/js-sdk/pull/1120)
18+
19+
## Design Decisions
20+
21+
1. **Single public method** accepting `hook_context:` and `evaluation_details:` keyword
22+
arguments — mirrors the `finally` hook stage signature for zero-friction integration.
23+
2. **Returns a Struct** (`EvaluationEvent`) with `name` and `attributes` fields — matches
24+
the SDK's existing Struct conventions (`ResolutionDetails`, `ClientMetadata`, etc.).
25+
3. **Constants in the Telemetry module directly** — flat namespace matching Go SDK and
26+
existing Ruby SDK patterns (e.g., `Provider::Reason`).
27+
4. **Hard-coded metadata mappings only** — maps `contextId`, `flagSetId`, `version` from
28+
flag metadata to OTel keys. Unknown metadata keys are ignored. Custom attributes can
29+
be added via hooks in ruby-sdk-contrib.
30+
5. **No third-party dependencies** — pure data transformation using only standard library.
31+
32+
## File Structure
33+
34+
- `lib/open_feature/sdk/telemetry.rb` — module with constants, struct, and utility function
35+
- `spec/open_feature/sdk/telemetry_spec.rb` — tests
36+
- `lib/open_feature/sdk.rb` — add `require_relative "sdk/telemetry"`
37+
38+
## Constants
39+
40+
```ruby
41+
EVENT_NAME = "feature_flag.evaluation"
42+
43+
FLAG_KEY = "feature_flag.key"
44+
CONTEXT_ID_KEY = "feature_flag.context.id"
45+
ERROR_MESSAGE_KEY = "error.message"
46+
ERROR_TYPE_KEY = "error.type"
47+
PROVIDER_NAME_KEY = "feature_flag.provider.name"
48+
RESULT_REASON_KEY = "feature_flag.result.reason"
49+
RESULT_VALUE_KEY = "feature_flag.result.value"
50+
RESULT_VARIANT_KEY = "feature_flag.result.variant"
51+
FLAG_SET_ID_KEY = "feature_flag.set.id"
52+
VERSION_KEY = "feature_flag.version"
53+
```
54+
55+
## Public API
56+
57+
```ruby
58+
OpenFeature::SDK::Telemetry.create_evaluation_event(
59+
hook_context:, # Hooks::HookContext
60+
evaluation_details: # EvaluationDetails or nil
61+
) # => EvaluationEvent
62+
```
63+
64+
Returns `EvaluationEvent = Struct.new(:name, :attributes, keyword_init: true)`.
65+
66+
## Attribute Population Rules
67+
68+
| Attribute | Source | Condition |
69+
|-----------|--------|-----------|
70+
| `feature_flag.key` | `hook_context.flag_key` | Always |
71+
| `feature_flag.provider.name` | `hook_context.provider_metadata.name` | When present |
72+
| `feature_flag.result.variant` | `evaluation_details.variant` | When present (takes precedence over value) |
73+
| `feature_flag.result.value` | `evaluation_details.value` | Only when variant is nil |
74+
| `feature_flag.result.reason` | `evaluation_details.reason.downcase` | When present |
75+
| `error.type` | `evaluation_details.error_code.downcase` | When error occurred |
76+
| `error.message` | `evaluation_details.error_message` | When error occurred |
77+
| `feature_flag.context.id` | `targeting_key` or metadata `contextId` | Metadata takes precedence |
78+
| `feature_flag.set.id` | metadata `flagSetId` | When present in flag_metadata |
79+
| `feature_flag.version` | metadata `version` | When present in flag_metadata |
80+
81+
## Error Handling
82+
83+
No defensive `rescue` in the utility — it is a pure data transformation. Nil inputs
84+
are handled via guard clauses. The calling hook is responsible for exception safety
85+
(consistent with the existing hook executor pattern).
86+
87+
## Test Plan
88+
89+
1. Happy path with all attributes populated
90+
2. Variant vs value precedence
91+
3. Enum downcasing (reason and error_code)
92+
4. Error attributes present only on error
93+
5. Nil evaluation_details
94+
6. Nil/empty flag_metadata
95+
7. Metadata contextId overrides targeting_key
96+
8. Targeting key fallback when no contextId
97+
9. Unknown metadata keys ignored
98+
10. Return type verification

0 commit comments

Comments
 (0)