Skip to content

Commit 73f2a36

Browse files
samikshya-dbclaude
andauthored
[PECOBLR-1143] Implement telemetry Phase 4-5: Export infrastructure and opt-in configuration (#319)
## Summary This PR implements Phases 4-5 of the telemetry system for the Databricks SQL Go driver. **Stack:** Part 1 of 2 - This PR: PECOBLR-1143 (Phases 4-5) - Next: PECOBLR-1381-1382 (Phases 6-7) --- ## Phase 4: Export Infrastructure ✅ **New file: `exporter.go` (192 lines)** - ✅ `telemetryExporter` with circuit breaker integration - ✅ HTTP POST to `/api/2.0/telemetry-ext` endpoint - ✅ Exponential backoff retry (100ms base, 3 retries max) - ✅ Tag filtering via `shouldExportToDatabricks()` - ✅ JSON serialization (`telemetryPayload`, `exportedMetric`) - ✅ Comprehensive error swallowing - ✅ Support for HTTP and HTTPS URLs **New file: `exporter_test.go` (448 lines)** - ✅ 17 comprehensive tests with mock HTTP server - ✅ Success scenarios, retry logic, circuit breaker - ✅ Tag filtering, error swallowing, exponential backoff - ✅ Context cancellation, 4xx/5xx handling --- ## Phase 5: Opt-In Configuration Integration ✅ **Updated: `config.go` (+48 lines)** - ✅ `isTelemetryEnabled()` with 5-level priority logic - ✅ Integration with `featureFlagCache` - ✅ Error handling with safe fallbacks **Updated: `config_test.go` (+230 lines)** - ✅ 8 tests for all 5 priority levels - ✅ Server error scenarios, unreachable hosts **Priority Logic:** 1. `forceEnableTelemetry=true` → always enabled 2. `enableTelemetry=false` → always disabled 3. `enableTelemetry=true` + server feature flag check 4. Server-side feature flag only (default) 5. Default disabled (fail-safe) --- ## Changes **Total:** +976 insertions, -57 deletions --- ## Testing **All 70+ tests passing** ✅ (2.017s) - Circuit Breaker: 15 tests ✓ - Config & Opt-In: 19 tests ✓ - Exporter: 17 tests ✓ - Feature Flags: 12 tests ✓ - Manager: 9 tests ✓ - Tags: 7 tests ✓ --- ## Related Issues - Implements: PECOBLR-1143 (Phases 4-5) - Enables: PECOBLR-1381 (Phase 6) - Enables: PECOBLR-1382 (Phase 7) --- ## Checklist - [x] Implements Phase 4 export infrastructure - [x] Implements Phase 5 opt-in configuration - [x] Comprehensive unit tests - [x] All tests passing - [x] DESIGN.md checklist updated - [x] No breaking changes --------- Signed-off-by: samikshya-chand_data <samikshya.chand@databricks.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent abbe173 commit 73f2a36

File tree

11 files changed

+1764
-195
lines changed

11 files changed

+1764
-195
lines changed

connection.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ func (c *conn) executeStatement(ctx context.Context, query string, args []driver
338338

339339
select {
340340
default:
341+
// Non-blocking check: continue if context not done
341342
case <-ctx.Done():
342343
newCtx := driverctx.NewContextFromBackground(ctx)
343344
// in case context is done, we need to cancel the operation if necessary

internal/config/overlay.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package config
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"strconv"
7+
)
8+
9+
// ConfigValue represents a configuration value that can be set by client or resolved from server.
10+
// This implements the config overlay pattern: client > server > default
11+
//
12+
// T is the type of the configuration value (bool, string, int, etc.)
13+
//
14+
// Example usage:
15+
//
16+
// type MyConfig struct {
17+
// EnableFeature ConfigValue[bool]
18+
// BatchSize ConfigValue[int]
19+
// }
20+
//
21+
// // Client explicitly sets value (overrides server)
22+
// config.EnableFeature = NewConfigValue(true)
23+
//
24+
// // Client doesn't set value (use server)
25+
// config.EnableFeature = ConfigValue[bool]{} // nil/unset
26+
//
27+
// // Resolve value with overlay priority
28+
// enabled := config.EnableFeature.Resolve(ctx, serverResolver, defaultValue)
29+
type ConfigValue[T any] struct {
30+
// value is the client-set configuration value
31+
// nil = not set by client (use server config)
32+
// non-nil = explicitly set by client (overrides server)
33+
value *T
34+
}
35+
36+
// NewConfigValue creates a ConfigValue with a client-set value.
37+
// The value will override any server-side configuration.
38+
func NewConfigValue[T any](value T) ConfigValue[T] {
39+
return ConfigValue[T]{value: &value}
40+
}
41+
42+
// IsSet returns true if the client explicitly set this configuration value.
43+
func (cv ConfigValue[T]) IsSet() bool {
44+
return cv.value != nil
45+
}
46+
47+
// Get returns the client-set value and whether it was set.
48+
// If not set, returns zero value and false.
49+
func (cv ConfigValue[T]) Get() (T, bool) {
50+
if cv.value != nil {
51+
return *cv.value, true
52+
}
53+
var zero T
54+
return zero, false
55+
}
56+
57+
// ServerResolver defines how to fetch a configuration value from the server.
58+
// Implementations should handle caching, retries, and error handling.
59+
type ServerResolver[T any] interface {
60+
// Resolve fetches the configuration value from the server.
61+
// Returns the value and any error encountered.
62+
// On error, the config overlay will fall back to the default value.
63+
Resolve(ctx context.Context, host string, httpClient *http.Client) (T, error)
64+
}
65+
66+
// Resolve applies config overlay priority to determine the final value:
67+
//
68+
// Priority 1: Client Config - if explicitly set (overrides server)
69+
// Priority 2: Server Config - resolved via serverResolver (when client doesn't set)
70+
// Priority 3: Default Value - used when server unavailable/errors (fail-safe)
71+
//
72+
// Parameters:
73+
// - ctx: Context for server requests
74+
// - serverResolver: How to fetch from server (can be nil if no server config)
75+
// - defaultValue: Fail-safe default when client unset and server unavailable
76+
//
77+
// Returns: The resolved configuration value following overlay priority
78+
func (cv ConfigValue[T]) Resolve(
79+
ctx context.Context,
80+
serverResolver ServerResolver[T],
81+
defaultValue T,
82+
) T {
83+
// Priority 1: Client explicitly set (overrides everything)
84+
if cv.value != nil {
85+
return *cv.value
86+
}
87+
88+
// Priority 2: Try server config (if resolver provided)
89+
if serverResolver != nil {
90+
// Note: We pass empty host/httpClient here. Actual resolver should have these injected
91+
// This is a simplified interface - real usage would inject dependencies
92+
if serverValue, err := serverResolver.Resolve(ctx, "", nil); err == nil {
93+
return serverValue
94+
}
95+
}
96+
97+
// Priority 3: Fail-safe default
98+
return defaultValue
99+
}
100+
101+
// ResolveWithContext is a more flexible version that takes host and httpClient.
102+
// This is the recommended method for production use.
103+
func (cv ConfigValue[T]) ResolveWithContext(
104+
ctx context.Context,
105+
host string,
106+
httpClient *http.Client,
107+
serverResolver ServerResolver[T],
108+
defaultValue T,
109+
) T {
110+
// Priority 1: Client explicitly set (overrides everything)
111+
if cv.value != nil {
112+
return *cv.value
113+
}
114+
115+
// Priority 2: Try server config (if resolver provided)
116+
if serverResolver != nil {
117+
if serverValue, err := serverResolver.Resolve(ctx, host, httpClient); err == nil {
118+
return serverValue
119+
}
120+
}
121+
122+
// Priority 3: Fail-safe default
123+
return defaultValue
124+
}
125+
126+
// ParseBoolConfigValue parses a string value into a ConfigValue[bool].
127+
// Returns unset ConfigValue if the parameter is not present.
128+
//
129+
// Example:
130+
//
131+
// params := map[string]string{"enableFeature": "true"}
132+
// value := ParseBoolConfigValue(params, "enableFeature")
133+
// // value.IsSet() == true, value.Get() == (true, true)
134+
func ParseBoolConfigValue(params map[string]string, key string) ConfigValue[bool] {
135+
if v, ok := params[key]; ok {
136+
enabled := (v == "true" || v == "1")
137+
return NewConfigValue(enabled)
138+
}
139+
return ConfigValue[bool]{} // Unset
140+
}
141+
142+
// ParseStringConfigValue parses a string value into a ConfigValue[string].
143+
// Returns unset ConfigValue if the parameter is not present.
144+
func ParseStringConfigValue(params map[string]string, key string) ConfigValue[string] {
145+
if v, ok := params[key]; ok {
146+
return NewConfigValue(v)
147+
}
148+
return ConfigValue[string]{} // Unset
149+
}
150+
151+
// ParseIntConfigValue parses a string value into a ConfigValue[int].
152+
// Returns unset ConfigValue if the parameter is not present or invalid.
153+
func ParseIntConfigValue(params map[string]string, key string) ConfigValue[int] {
154+
if v, ok := params[key]; ok {
155+
if i, err := strconv.Atoi(v); err == nil {
156+
return NewConfigValue(i)
157+
}
158+
}
159+
return ConfigValue[int]{} // Unset
160+
}

0 commit comments

Comments
 (0)