Skip to content

Commit 65665ea

Browse files
Add OpenTelemetry Configuration Management (#861)
1 parent 3fd2a02 commit 65665ea

15 files changed

+605
-8
lines changed

cmd/thv/app/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ func init() {
141141
configCmd.AddCommand(getRegistryURLCmd)
142142
configCmd.AddCommand(unsetRegistryURLCmd)
143143

144+
// Add OTEL parent command to config
145+
configCmd.AddCommand(OtelCmd)
144146
}
145147

146148
func registerClientCmdFunc(cmd *cobra.Command, args []string) error {

cmd/thv/app/otel.go

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package app
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/stacklok/toolhive/pkg/config"
11+
)
12+
13+
// OtelCmd is the parent command for OpenTelemetry configuration
14+
var OtelCmd = &cobra.Command{
15+
Use: "otel",
16+
Short: "Manage OpenTelemetry configuration",
17+
Long: "Configure OpenTelemetry settings for observability and monitoring of MCP servers.",
18+
}
19+
20+
var setOtelEndpointCmd = &cobra.Command{
21+
Use: "set-endpoint <endpoint>",
22+
Short: "Set the OpenTelemetry endpoint URL",
23+
Long: `Set the OpenTelemetry OTLP endpoint URL for tracing and metrics.
24+
This endpoint will be used by default when running MCP servers unless overridden by the --otel-endpoint flag.
25+
26+
Example:
27+
thv config otel set-endpoint https://api.honeycomb.io`,
28+
Args: cobra.ExactArgs(1),
29+
RunE: setOtelEndpointCmdFunc,
30+
}
31+
32+
var getOtelEndpointCmd = &cobra.Command{
33+
Use: "get-endpoint",
34+
Short: "Get the currently configured OpenTelemetry endpoint",
35+
Long: "Display the OpenTelemetry endpoint URL that is currently configured.",
36+
RunE: getOtelEndpointCmdFunc,
37+
}
38+
39+
var unsetOtelEndpointCmd = &cobra.Command{
40+
Use: "unset-endpoint",
41+
Short: "Remove the configured OpenTelemetry endpoint",
42+
Long: "Remove the OpenTelemetry endpoint configuration.",
43+
RunE: unsetOtelEndpointCmdFunc,
44+
}
45+
46+
var setOtelSamplingRateCmd = &cobra.Command{
47+
Use: "set-sampling-rate <rate>",
48+
Short: "Set the OpenTelemetry sampling rate",
49+
Long: `Set the OpenTelemetry trace sampling rate (between 0.0 and 1.0).
50+
This sampling rate will be used by default when running MCP servers unless overridden by the --otel-sampling-rate flag.
51+
52+
Example:
53+
thv config otel set-sampling-rate 0.1`,
54+
Args: cobra.ExactArgs(1),
55+
RunE: setOtelSamplingRateCmdFunc,
56+
}
57+
58+
var getOtelSamplingRateCmd = &cobra.Command{
59+
Use: "get-sampling-rate",
60+
Short: "Get the currently configured OpenTelemetry sampling rate",
61+
Long: "Display the OpenTelemetry sampling rate that is currently configured.",
62+
RunE: getOtelSamplingRateCmdFunc,
63+
}
64+
65+
var unsetOtelSamplingRateCmd = &cobra.Command{
66+
Use: "unset-sampling-rate",
67+
Short: "Remove the configured OpenTelemetry sampling rate",
68+
Long: "Remove the OpenTelemetry sampling rate configuration.",
69+
RunE: unsetOtelSamplingRateCmdFunc,
70+
}
71+
72+
var setOtelEnvVarsCmd = &cobra.Command{
73+
Use: "set-env-vars <var1,var2,...>",
74+
Short: "Set the OpenTelemetry environment variables",
75+
Long: `Set the list of environment variable names to include in OpenTelemetry spans.
76+
These environment variables will be used by default when running MCP servers unless overridden by the --otel-env-vars flag.
77+
78+
Example:
79+
thv config otel set-env-vars USER,HOME,PATH`,
80+
Args: cobra.ExactArgs(1),
81+
RunE: setOtelEnvVarsCmdFunc,
82+
}
83+
84+
var getOtelEnvVarsCmd = &cobra.Command{
85+
Use: "get-env-vars",
86+
Short: "Get the currently configured OpenTelemetry environment variables",
87+
Long: "Display the OpenTelemetry environment variables that are currently configured.",
88+
RunE: getOtelEnvVarsCmdFunc,
89+
}
90+
91+
var unsetOtelEnvVarsCmd = &cobra.Command{
92+
Use: "unset-env-vars",
93+
Short: "Remove the configured OpenTelemetry environment variables",
94+
Long: "Remove the OpenTelemetry environment variables configuration.",
95+
RunE: unsetOtelEnvVarsCmdFunc,
96+
}
97+
98+
// init sets up the OTEL command hierarchy
99+
func init() {
100+
// Add OTEL subcommands to otel command
101+
OtelCmd.AddCommand(setOtelEndpointCmd)
102+
OtelCmd.AddCommand(getOtelEndpointCmd)
103+
OtelCmd.AddCommand(unsetOtelEndpointCmd)
104+
OtelCmd.AddCommand(setOtelSamplingRateCmd)
105+
OtelCmd.AddCommand(getOtelSamplingRateCmd)
106+
OtelCmd.AddCommand(unsetOtelSamplingRateCmd)
107+
OtelCmd.AddCommand(setOtelEnvVarsCmd)
108+
OtelCmd.AddCommand(getOtelEnvVarsCmd)
109+
OtelCmd.AddCommand(unsetOtelEnvVarsCmd)
110+
}
111+
112+
func setOtelEndpointCmdFunc(_ *cobra.Command, args []string) error {
113+
endpoint := args[0]
114+
115+
// The endpoint should not start with http:// or https://
116+
if endpoint != "" && (strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://")) {
117+
return fmt.Errorf("endpoint URL should not start with http:// or https://")
118+
}
119+
120+
// Update the configuration
121+
err := config.UpdateConfig(func(c *config.Config) {
122+
c.OTEL.Endpoint = endpoint
123+
})
124+
if err != nil {
125+
return fmt.Errorf("failed to update configuration: %w", err)
126+
}
127+
128+
fmt.Printf("Successfully set OpenTelemetry endpoint: %s\n", endpoint)
129+
return nil
130+
}
131+
132+
func getOtelEndpointCmdFunc(_ *cobra.Command, _ []string) error {
133+
cfg := config.GetConfig()
134+
135+
if cfg.OTEL.Endpoint == "" {
136+
fmt.Println("No OpenTelemetry endpoint is currently configured.")
137+
return nil
138+
}
139+
140+
fmt.Printf("Current OpenTelemetry endpoint: %s\n", cfg.OTEL.Endpoint)
141+
return nil
142+
}
143+
144+
func unsetOtelEndpointCmdFunc(_ *cobra.Command, _ []string) error {
145+
cfg := config.GetConfig()
146+
147+
if cfg.OTEL.Endpoint == "" {
148+
fmt.Println("No OpenTelemetry endpoint is currently configured.")
149+
return nil
150+
}
151+
152+
// Update the configuration
153+
err := config.UpdateConfig(func(c *config.Config) {
154+
c.OTEL.Endpoint = ""
155+
})
156+
if err != nil {
157+
return fmt.Errorf("failed to update configuration: %w", err)
158+
}
159+
160+
fmt.Println("Successfully removed OpenTelemetry endpoint configuration.")
161+
return nil
162+
}
163+
164+
func setOtelSamplingRateCmdFunc(_ *cobra.Command, args []string) error {
165+
rate, err := strconv.ParseFloat(args[0], 64)
166+
if err != nil {
167+
return fmt.Errorf("invalid sampling rate format: %w", err)
168+
}
169+
170+
// Validate the rate
171+
if rate < 0.0 || rate > 1.0 {
172+
return fmt.Errorf("sampling rate must be between 0.0 and 1.0")
173+
}
174+
175+
// Update the configuration
176+
err = config.UpdateConfig(func(c *config.Config) {
177+
c.OTEL.SamplingRate = rate
178+
})
179+
if err != nil {
180+
return fmt.Errorf("failed to update configuration: %w", err)
181+
}
182+
183+
fmt.Printf("Successfully set OpenTelemetry sampling rate: %f\n", rate)
184+
return nil
185+
}
186+
187+
func getOtelSamplingRateCmdFunc(_ *cobra.Command, _ []string) error {
188+
cfg := config.GetConfig()
189+
190+
if cfg.OTEL.SamplingRate == 0.0 {
191+
fmt.Println("No OpenTelemetry sampling rate is currently configured.")
192+
return nil
193+
}
194+
195+
fmt.Printf("Current OpenTelemetry sampling rate: %f\n", cfg.OTEL.SamplingRate)
196+
return nil
197+
}
198+
199+
func unsetOtelSamplingRateCmdFunc(_ *cobra.Command, _ []string) error {
200+
cfg := config.GetConfig()
201+
202+
if cfg.OTEL.SamplingRate == 0.0 {
203+
fmt.Println("No OpenTelemetry sampling rate is currently configured.")
204+
return nil
205+
}
206+
207+
// Update the configuration
208+
err := config.UpdateConfig(func(c *config.Config) {
209+
c.OTEL.SamplingRate = 0.0
210+
})
211+
if err != nil {
212+
return fmt.Errorf("failed to update configuration: %w", err)
213+
}
214+
215+
fmt.Println("Successfully removed OpenTelemetry sampling rate configuration.")
216+
return nil
217+
}
218+
219+
func setOtelEnvVarsCmdFunc(_ *cobra.Command, args []string) error {
220+
vars := strings.Split(args[0], ",")
221+
222+
// Trim whitespace from each variable name
223+
for i, varName := range vars {
224+
vars[i] = strings.TrimSpace(varName)
225+
}
226+
227+
// Update the configuration
228+
err := config.UpdateConfig(func(c *config.Config) {
229+
c.OTEL.EnvVars = vars
230+
})
231+
if err != nil {
232+
return fmt.Errorf("failed to update configuration: %w", err)
233+
}
234+
235+
fmt.Printf("Successfully set OpenTelemetry environment variables: %v\n", vars)
236+
return nil
237+
}
238+
239+
func getOtelEnvVarsCmdFunc(_ *cobra.Command, _ []string) error {
240+
cfg := config.GetConfig()
241+
242+
if len(cfg.OTEL.EnvVars) == 0 {
243+
fmt.Println("No OpenTelemetry environment variables are currently configured.")
244+
return nil
245+
}
246+
247+
fmt.Printf("Current OpenTelemetry environment variables: %v\n", cfg.OTEL.EnvVars)
248+
return nil
249+
}
250+
251+
func unsetOtelEnvVarsCmdFunc(_ *cobra.Command, _ []string) error {
252+
cfg := config.GetConfig()
253+
254+
if len(cfg.OTEL.EnvVars) == 0 {
255+
fmt.Println("No OpenTelemetry environment variables are currently configured.")
256+
return nil
257+
}
258+
259+
// Update the configuration
260+
err := config.UpdateConfig(func(c *config.Config) {
261+
c.OTEL.EnvVars = []string{}
262+
})
263+
if err != nil {
264+
return fmt.Errorf("failed to update configuration: %w", err)
265+
}
266+
267+
fmt.Println("Successfully removed OpenTelemetry environment variables configuration.")
268+
return nil
269+
}

cmd/thv/app/run.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/spf13/cobra"
99

10+
"github.com/stacklok/toolhive/pkg/config"
1011
"github.com/stacklok/toolhive/pkg/container"
1112
"github.com/stacklok/toolhive/pkg/logger"
1213
"github.com/stacklok/toolhive/pkg/permissions"
@@ -225,6 +226,25 @@ func runCmdFunc(cmd *cobra.Command, args []string) error {
225226
oidcJwksURL := GetStringFlagOrEmpty(cmd, "oidc-jwks-url")
226227
oidcClientID := GetStringFlagOrEmpty(cmd, "oidc-client-id")
227228

229+
// Get OTEL flag values with config fallbacks
230+
cfg := config.GetConfig()
231+
232+
// Use config values as fallbacks for OTEL flags if not explicitly set
233+
finalOtelEndpoint := runOtelEndpoint
234+
if !cmd.Flags().Changed("otel-endpoint") && cfg.OTEL.Endpoint != "" {
235+
finalOtelEndpoint = cfg.OTEL.Endpoint
236+
}
237+
238+
finalOtelSamplingRate := runOtelSamplingRate
239+
if !cmd.Flags().Changed("otel-sampling-rate") && cfg.OTEL.SamplingRate != 0.0 {
240+
finalOtelSamplingRate = cfg.OTEL.SamplingRate
241+
}
242+
243+
finalOtelEnvironmentVariables := runOtelEnvironmentVariables
244+
if !cmd.Flags().Changed("otel-env-vars") && len(cfg.OTEL.EnvVars) > 0 {
245+
finalOtelEnvironmentVariables = cfg.OTEL.EnvVars
246+
}
247+
228248
// Create container runtime
229249
rt, err := container.NewFactory().Create(ctx)
230250
if err != nil {
@@ -285,13 +305,13 @@ func runCmdFunc(cmd *cobra.Command, args []string) error {
285305
oidcAudience,
286306
oidcJwksURL,
287307
oidcClientID,
288-
runOtelEndpoint,
308+
finalOtelEndpoint,
289309
runOtelServiceName,
290-
runOtelSamplingRate,
310+
finalOtelSamplingRate,
291311
runOtelHeaders,
292312
runOtelInsecure,
293313
runOtelEnablePrometheusMetricsPath,
294-
runOtelEnvironmentVariables,
314+
finalOtelEnvironmentVariables,
295315
runIsolateNetwork,
296316
runK8sPodPatch,
297317
envVarValidator,

docs/cli/thv_config.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cli/thv_config_otel.md

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)