golf-mcp 0.1.7__py3-none-any.whl → 0.1.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of golf-mcp might be problematic. Click here for more details.

@@ -0,0 +1,19 @@
1
+ """Golf telemetry module for OpenTelemetry instrumentation."""
2
+
3
+ from golf.telemetry.instrumentation import (
4
+ instrument_tool,
5
+ instrument_resource,
6
+ instrument_prompt,
7
+ telemetry_lifespan,
8
+ init_telemetry,
9
+ get_tracer,
10
+ )
11
+
12
+ __all__ = [
13
+ "instrument_tool",
14
+ "instrument_resource",
15
+ "instrument_prompt",
16
+ "telemetry_lifespan",
17
+ "init_telemetry",
18
+ "get_tracer",
19
+ ]
@@ -0,0 +1,540 @@
1
+ """Component-level OpenTelemetry instrumentation for Golf-built servers."""
2
+
3
+ import os
4
+ import sys
5
+ import functools
6
+ from typing import Callable, Optional, TypeVar
7
+ from contextlib import asynccontextmanager
8
+ import asyncio
9
+
10
+ from opentelemetry import trace
11
+ from opentelemetry.sdk.trace import TracerProvider
12
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
13
+ from opentelemetry.sdk.resources import Resource
14
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
15
+ from opentelemetry.trace import Status, StatusCode, Span
16
+ from opentelemetry import baggage
17
+
18
+ T = TypeVar('T')
19
+
20
+ # Global tracer instance
21
+ _tracer: Optional[trace.Tracer] = None
22
+ _provider: Optional[TracerProvider] = None
23
+ _instrumented_tools = []
24
+
25
+ def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProvider]:
26
+ """Initialize OpenTelemetry with environment-based configuration.
27
+
28
+ Returns None if required environment variables are not set.
29
+ """
30
+ global _provider
31
+
32
+ # Check for required environment variables based on exporter type
33
+ exporter_type = os.environ.get("OTEL_TRACES_EXPORTER", "console").lower()
34
+
35
+ # For OTLP HTTP exporter, check if endpoint is configured
36
+ if exporter_type == "otlp_http":
37
+ endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
38
+ if not endpoint:
39
+ print(f"[WARNING] OpenTelemetry tracing is disabled: OTEL_EXPORTER_OTLP_ENDPOINT is not set for OTLP HTTP exporter")
40
+ return None
41
+
42
+ # Create resource with service information
43
+ resource_attributes = {
44
+ "service.name": os.environ.get("OTEL_SERVICE_NAME", service_name),
45
+ "service.version": os.environ.get("SERVICE_VERSION", "1.0.0"),
46
+ "service.instance.id": os.environ.get("SERVICE_INSTANCE_ID", "default"),
47
+ }
48
+ resource = Resource.create(resource_attributes)
49
+
50
+ # Create provider
51
+ provider = TracerProvider(resource=resource)
52
+
53
+ # Configure exporter based on type
54
+ try:
55
+ if exporter_type == "otlp_http":
56
+ endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces")
57
+ headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
58
+
59
+ # Parse headers if provided
60
+ header_dict = {}
61
+ if headers:
62
+ for header in headers.split(","):
63
+ if "=" in header:
64
+ key, value = header.split("=", 1)
65
+ header_dict[key.strip()] = value.strip()
66
+
67
+ exporter = OTLPSpanExporter(
68
+ endpoint=endpoint,
69
+ headers=header_dict if header_dict else None
70
+ )
71
+ else:
72
+ # Default to console exporter
73
+ exporter = ConsoleSpanExporter(out=sys.stderr)
74
+ except Exception as e:
75
+ import traceback
76
+ traceback.print_exc()
77
+ raise
78
+
79
+ # Add batch processor for better performance
80
+ try:
81
+ processor = BatchSpanProcessor(
82
+ exporter,
83
+ max_queue_size=2048,
84
+ schedule_delay_millis=1000, # Export every 1 second instead of default 5 seconds
85
+ max_export_batch_size=512,
86
+ export_timeout_millis=5000
87
+ )
88
+ provider.add_span_processor(processor)
89
+ except Exception as e:
90
+ import traceback
91
+ traceback.print_exc()
92
+ raise
93
+
94
+ # Set as global provider
95
+ try:
96
+ # Check if a provider is already set to avoid the warning
97
+ existing_provider = trace.get_tracer_provider()
98
+ if existing_provider is None or str(type(existing_provider).__name__) == 'ProxyTracerProvider':
99
+ # Only set if no provider exists or it's the default proxy provider
100
+ trace.set_tracer_provider(provider)
101
+ _provider = provider
102
+ except Exception as e:
103
+ import traceback
104
+ traceback.print_exc()
105
+ raise
106
+
107
+ # Create a test span to verify everything is working
108
+ try:
109
+ test_tracer = provider.get_tracer("golf.telemetry.test", "1.0.0")
110
+ with test_tracer.start_as_current_span("startup.test") as span:
111
+ span.set_attribute("test", True)
112
+ span.set_attribute("service.name", service_name)
113
+ span.set_attribute("exporter.type", exporter_type)
114
+ span.set_attribute("endpoint", os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "not set"))
115
+ except Exception as e:
116
+ import traceback
117
+ traceback.print_exc()
118
+
119
+ return provider
120
+
121
+ def get_tracer() -> trace.Tracer:
122
+ """Get or create the global tracer instance."""
123
+ global _tracer, _provider
124
+
125
+ # If no provider is set, telemetry is disabled - return no-op tracer
126
+ if _provider is None:
127
+ return trace.get_tracer("golf.mcp.components.noop", "1.0.0")
128
+
129
+ if _tracer is None:
130
+ _tracer = trace.get_tracer("golf.mcp.components", "1.0.0")
131
+ return _tracer
132
+
133
+ def _add_component_attributes(span: Span, component_type: str, component_name: str, **kwargs):
134
+ """Add standard component attributes to a span."""
135
+ span.set_attribute("mcp.component.type", component_type)
136
+ span.set_attribute("mcp.component.name", component_name)
137
+
138
+ # Add any additional attributes
139
+ for key, value in kwargs.items():
140
+ if value is not None:
141
+ span.set_attribute(f"mcp.component.{key}", str(value))
142
+
143
+ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
144
+ """Instrument a tool function with OpenTelemetry tracing."""
145
+ global _provider
146
+
147
+ # If telemetry is disabled, return the original function
148
+ if _provider is None:
149
+ return func
150
+
151
+ tracer = get_tracer()
152
+
153
+ @functools.wraps(func)
154
+ async def async_wrapper(*args, **kwargs):
155
+ span = tracer.start_span(f"tool.{tool_name}")
156
+
157
+ # Activate the span in the current context
158
+ from opentelemetry import context
159
+ token = context.attach(trace.set_span_in_context(span))
160
+
161
+ try:
162
+ _add_component_attributes(span, "tool", tool_name,
163
+ args_count=len(args),
164
+ kwargs_count=len(kwargs))
165
+
166
+ # Extract Context parameter if present - this should have MCP session info
167
+ ctx = kwargs.get('ctx')
168
+ if ctx:
169
+ if hasattr(ctx, 'request_id'):
170
+ span.set_attribute("mcp.request.id", ctx.request_id)
171
+ if hasattr(ctx, 'session_id'):
172
+ span.set_attribute("mcp.session.id", ctx.session_id)
173
+ # Try to find any session-related attributes
174
+ for attr in dir(ctx):
175
+ if 'session' in attr.lower() and not attr.startswith('_'):
176
+ value = getattr(ctx, attr, None)
177
+ if value:
178
+ span.set_attribute(f"mcp.context.{attr}", str(value))
179
+
180
+ # Also check baggage for session ID
181
+ session_id_from_baggage = baggage.get_baggage("mcp.session.id")
182
+ if session_id_from_baggage:
183
+ span.set_attribute("mcp.session.id", session_id_from_baggage)
184
+
185
+ # Add tool arguments as span attributes (be careful with sensitive data)
186
+ for i, arg in enumerate(args):
187
+ if isinstance(arg, (str, int, float, bool)) or arg is None:
188
+ span.set_attribute(f"tool.arg.{i}", str(arg))
189
+ elif hasattr(arg, '__dict__'):
190
+ # For objects, just record the type
191
+ span.set_attribute(f"tool.arg.{i}.type", type(arg).__name__)
192
+
193
+ # Add named arguments
194
+ for key, value in kwargs.items():
195
+ if key != 'ctx':
196
+ if value is None:
197
+ span.set_attribute(f"tool.kwarg.{key}", "null")
198
+ elif isinstance(value, (str, int, float, bool)):
199
+ span.set_attribute(f"tool.kwarg.{key}", str(value))
200
+ elif isinstance(value, (list, tuple)):
201
+ span.set_attribute(f"tool.kwarg.{key}", f"[{len(value)} items]")
202
+ elif isinstance(value, dict):
203
+ span.set_attribute(f"tool.kwarg.{key}", f"{{dict with {len(value)} keys}}")
204
+ else:
205
+ # For other types, at least record the type
206
+ span.set_attribute(f"tool.kwarg.{key}.type", type(value).__name__)
207
+
208
+ try:
209
+ result = await func(*args, **kwargs)
210
+ span.set_status(Status(StatusCode.OK))
211
+
212
+ # Capture result metadata
213
+ if result is not None:
214
+ if isinstance(result, (str, int, float, bool)):
215
+ span.set_attribute("tool.result", str(result))
216
+ elif isinstance(result, list):
217
+ span.set_attribute("tool.result.count", len(result))
218
+ span.set_attribute("tool.result.type", "list")
219
+ elif isinstance(result, dict):
220
+ span.set_attribute("tool.result.keys", ",".join(result.keys()) if len(result) < 10 else f"{len(result)} keys")
221
+ span.set_attribute("tool.result.type", "dict")
222
+ elif hasattr(result, '__len__'):
223
+ span.set_attribute("tool.result.length", len(result))
224
+
225
+ # For any result, record its type
226
+ span.set_attribute("tool.result.class", type(result).__name__)
227
+
228
+ return result
229
+ except Exception as e:
230
+ span.record_exception(e)
231
+ span.set_status(Status(StatusCode.ERROR, str(e)))
232
+ raise
233
+ finally:
234
+ # End the span and detach context
235
+ span.end()
236
+ context.detach(token)
237
+
238
+ # Force flush the provider to ensure spans are exported
239
+ global _provider
240
+ if _provider:
241
+ try:
242
+ _provider.force_flush(timeout_millis=1000)
243
+ except Exception as e:
244
+ pass
245
+
246
+ @functools.wraps(func)
247
+ def sync_wrapper(*args, **kwargs):
248
+ span = tracer.start_span(f"tool.{tool_name}")
249
+
250
+ # Activate the span in the current context
251
+ from opentelemetry import context
252
+ token = context.attach(trace.set_span_in_context(span))
253
+
254
+ try:
255
+ _add_component_attributes(span, "tool", tool_name,
256
+ args_count=len(args),
257
+ kwargs_count=len(kwargs))
258
+
259
+ # Extract Context parameter if present - this should have MCP session info
260
+ ctx = kwargs.get('ctx')
261
+ if ctx:
262
+ if hasattr(ctx, 'request_id'):
263
+ span.set_attribute("mcp.request.id", ctx.request_id)
264
+ if hasattr(ctx, 'session_id'):
265
+ span.set_attribute("mcp.session.id", ctx.session_id)
266
+ # Try to find any session-related attributes
267
+ for attr in dir(ctx):
268
+ if 'session' in attr.lower() and not attr.startswith('_'):
269
+ value = getattr(ctx, attr, None)
270
+ if value:
271
+ span.set_attribute(f"mcp.context.{attr}", str(value))
272
+
273
+ # Also check baggage for session ID
274
+ session_id_from_baggage = baggage.get_baggage("mcp.session.id")
275
+ if session_id_from_baggage:
276
+ span.set_attribute("mcp.session.id", session_id_from_baggage)
277
+
278
+ # Add tool arguments as span attributes (be careful with sensitive data)
279
+ for i, arg in enumerate(args):
280
+ if isinstance(arg, (str, int, float, bool)) or arg is None:
281
+ span.set_attribute(f"tool.arg.{i}", str(arg))
282
+ elif hasattr(arg, '__dict__'):
283
+ # For objects, just record the type
284
+ span.set_attribute(f"tool.arg.{i}.type", type(arg).__name__)
285
+
286
+ # Add named arguments
287
+ for key, value in kwargs.items():
288
+ if key != 'ctx':
289
+ if value is None:
290
+ span.set_attribute(f"tool.kwarg.{key}", "null")
291
+ elif isinstance(value, (str, int, float, bool)):
292
+ span.set_attribute(f"tool.kwarg.{key}", str(value))
293
+ elif isinstance(value, (list, tuple)):
294
+ span.set_attribute(f"tool.kwarg.{key}", f"[{len(value)} items]")
295
+ elif isinstance(value, dict):
296
+ span.set_attribute(f"tool.kwarg.{key}", f"{{dict with {len(value)} keys}}")
297
+ else:
298
+ # For other types, at least record the type
299
+ span.set_attribute(f"tool.kwarg.{key}.type", type(value).__name__)
300
+
301
+ try:
302
+ result = func(*args, **kwargs)
303
+ span.set_status(Status(StatusCode.OK))
304
+
305
+ # Capture result metadata
306
+ if result is not None:
307
+ if isinstance(result, (str, int, float, bool)):
308
+ span.set_attribute("tool.result", str(result))
309
+ elif isinstance(result, list):
310
+ span.set_attribute("tool.result.count", len(result))
311
+ span.set_attribute("tool.result.type", "list")
312
+ elif isinstance(result, dict):
313
+ span.set_attribute("tool.result.keys", ",".join(result.keys()) if len(result) < 10 else f"{len(result)} keys")
314
+ span.set_attribute("tool.result.type", "dict")
315
+ elif hasattr(result, '__len__'):
316
+ span.set_attribute("tool.result.length", len(result))
317
+
318
+ # For any result, record its type
319
+ span.set_attribute("tool.result.class", type(result).__name__)
320
+
321
+ return result
322
+ except Exception as e:
323
+ span.record_exception(e)
324
+ span.set_status(Status(StatusCode.ERROR, str(e)))
325
+ raise
326
+ finally:
327
+ # End the span and detach context
328
+ span.end()
329
+ context.detach(token)
330
+
331
+ # Force flush the provider to ensure spans are exported
332
+ global _provider
333
+ if _provider:
334
+ try:
335
+ _provider.force_flush(timeout_millis=1000)
336
+ except Exception as e:
337
+ pass
338
+
339
+ # Return appropriate wrapper based on function type
340
+ if asyncio.iscoroutinefunction(func):
341
+ return async_wrapper
342
+ else:
343
+ return sync_wrapper
344
+
345
+ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[..., T]:
346
+ """Instrument a resource function with OpenTelemetry tracing."""
347
+ global _provider
348
+
349
+ # If telemetry is disabled, return the original function
350
+ if _provider is None:
351
+ return func
352
+
353
+ tracer = get_tracer()
354
+
355
+ # Determine if this is a template based on URI pattern
356
+ is_template = '{' in resource_uri
357
+
358
+ @functools.wraps(func)
359
+ async def async_wrapper(*args, **kwargs):
360
+ span_name = "resource.template.read" if is_template else "resource.read"
361
+ with tracer.start_as_current_span(span_name) as span:
362
+ _add_component_attributes(span, "resource", resource_uri,
363
+ is_template=is_template)
364
+
365
+ # Extract Context parameter if present
366
+ ctx = kwargs.get('ctx')
367
+ if ctx and hasattr(ctx, 'request_id'):
368
+ span.set_attribute("mcp.request.id", ctx.request_id)
369
+
370
+ try:
371
+ result = await func(*args, **kwargs)
372
+ span.set_status(Status(StatusCode.OK))
373
+
374
+ # Add result size if applicable
375
+ if hasattr(result, '__len__'):
376
+ span.set_attribute("mcp.resource.size", len(result))
377
+
378
+ return result
379
+ except Exception as e:
380
+ span.record_exception(e)
381
+ span.set_status(Status(StatusCode.ERROR, str(e)))
382
+ raise
383
+
384
+ @functools.wraps(func)
385
+ def sync_wrapper(*args, **kwargs):
386
+ span_name = "resource.template.read" if is_template else "resource.read"
387
+ with tracer.start_as_current_span(span_name) as span:
388
+ _add_component_attributes(span, "resource", resource_uri,
389
+ is_template=is_template)
390
+
391
+ # Extract Context parameter if present
392
+ ctx = kwargs.get('ctx')
393
+ if ctx and hasattr(ctx, 'request_id'):
394
+ span.set_attribute("mcp.request.id", ctx.request_id)
395
+
396
+ try:
397
+ result = func(*args, **kwargs)
398
+ span.set_status(Status(StatusCode.OK))
399
+
400
+ # Add result size if applicable
401
+ if hasattr(result, '__len__'):
402
+ span.set_attribute("mcp.resource.size", len(result))
403
+
404
+ return result
405
+ except Exception as e:
406
+ span.record_exception(e)
407
+ span.set_status(Status(StatusCode.ERROR, str(e)))
408
+ raise
409
+
410
+ if asyncio.iscoroutinefunction(func):
411
+ return async_wrapper
412
+ else:
413
+ return sync_wrapper
414
+
415
+ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[..., T]:
416
+ """Instrument a prompt function with OpenTelemetry tracing."""
417
+ global _provider
418
+
419
+ # If telemetry is disabled, return the original function
420
+ if _provider is None:
421
+ return func
422
+
423
+ tracer = get_tracer()
424
+
425
+ @functools.wraps(func)
426
+ async def async_wrapper(*args, **kwargs):
427
+ with tracer.start_as_current_span(f"prompt.{prompt_name}") as span:
428
+ _add_component_attributes(span, "prompt", prompt_name)
429
+
430
+ # Extract Context parameter if present
431
+ ctx = kwargs.get('ctx')
432
+ if ctx and hasattr(ctx, 'request_id'):
433
+ span.set_attribute("mcp.request.id", ctx.request_id)
434
+
435
+ try:
436
+ result = await func(*args, **kwargs)
437
+ span.set_status(Status(StatusCode.OK))
438
+
439
+ # Add message count if result is a list
440
+ if isinstance(result, list):
441
+ span.set_attribute("mcp.prompt.message_count", len(result))
442
+
443
+ return result
444
+ except Exception as e:
445
+ span.record_exception(e)
446
+ span.set_status(Status(StatusCode.ERROR, str(e)))
447
+ raise
448
+
449
+ @functools.wraps(func)
450
+ def sync_wrapper(*args, **kwargs):
451
+ with tracer.start_as_current_span(f"prompt.{prompt_name}") as span:
452
+ _add_component_attributes(span, "prompt", prompt_name)
453
+
454
+ # Extract Context parameter if present
455
+ ctx = kwargs.get('ctx')
456
+ if ctx and hasattr(ctx, 'request_id'):
457
+ span.set_attribute("mcp.request.id", ctx.request_id)
458
+
459
+ try:
460
+ result = func(*args, **kwargs)
461
+ span.set_status(Status(StatusCode.OK))
462
+
463
+ # Add message count if result is a list
464
+ if isinstance(result, list):
465
+ span.set_attribute("mcp.prompt.message_count", len(result))
466
+
467
+ return result
468
+ except Exception as e:
469
+ span.record_exception(e)
470
+ span.set_status(Status(StatusCode.ERROR, str(e)))
471
+ raise
472
+
473
+ if asyncio.iscoroutinefunction(func):
474
+ return async_wrapper
475
+ else:
476
+ return sync_wrapper
477
+
478
+ @asynccontextmanager
479
+ async def telemetry_lifespan(mcp_instance):
480
+ """Simplified lifespan for telemetry initialization and cleanup."""
481
+ global _provider, _instrumented_tools
482
+
483
+ # Initialize telemetry with the server name
484
+ provider = init_telemetry(service_name=mcp_instance.name)
485
+
486
+ # If provider is None, telemetry is disabled
487
+ if provider is None:
488
+ # Just yield without any telemetry setup
489
+ yield
490
+ return
491
+
492
+ # Try to add session tracking middleware if possible
493
+ try:
494
+ from starlette.middleware.base import BaseHTTPMiddleware
495
+ from starlette.requests import Request
496
+
497
+ class SessionTracingMiddleware(BaseHTTPMiddleware):
498
+ async def dispatch(self, request: Request, call_next):
499
+ # Extract session ID from query params
500
+ session_id = request.query_params.get('session_id')
501
+ if session_id:
502
+ # Add to baggage for propagation
503
+ ctx = baggage.set_baggage("mcp.session.id", session_id)
504
+ from opentelemetry import context
505
+ token = context.attach(ctx)
506
+
507
+ # Also create a span for the HTTP request
508
+ tracer = get_tracer()
509
+ with tracer.start_as_current_span(f"http.{request.method} {request.url.path}") as span:
510
+ span.set_attribute("http.method", request.method)
511
+ span.set_attribute("http.url", str(request.url))
512
+ span.set_attribute("http.session_id", session_id)
513
+ span.set_attribute("mcp.session.id", session_id)
514
+
515
+ try:
516
+ response = await call_next(request)
517
+ span.set_attribute("http.status_code", response.status_code)
518
+ return response
519
+ finally:
520
+ context.detach(token)
521
+ else:
522
+ return await call_next(request)
523
+
524
+ # Try to add middleware to FastMCP app if it has Starlette app
525
+ if hasattr(mcp_instance, 'app') or hasattr(mcp_instance, '_app'):
526
+ app = getattr(mcp_instance, 'app', getattr(mcp_instance, '_app', None))
527
+ if app and hasattr(app, 'add_middleware'):
528
+ app.add_middleware(SessionTracingMiddleware)
529
+ except Exception:
530
+ pass
531
+
532
+ try:
533
+ # Yield control back to FastMCP
534
+ yield
535
+ finally:
536
+ # Cleanup - shutdown the provider
537
+ if _provider and hasattr(_provider, 'shutdown'):
538
+ _provider.force_flush()
539
+ _provider.shutdown()
540
+ _provider = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: golf-mcp
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: Framework for building MCP servers
5
5
  Author-email: Antoni Gmitruk <antoni@golf.dev>
6
6
  License-Expression: Apache-2.0
@@ -177,6 +177,11 @@ The `golf.json` file is the heart of your Golf project configuration. Here's wha
177
177
  // - "sse": Server-Sent Events (recommended for web clients)
178
178
  // - "streamable-http": HTTP with streaming support
179
179
  // - "stdio": Standard I/O (for CLI integration)
180
+
181
+ // OpenTelemetry Configuration (optional)
182
+ "opentelemetry_enabled": false, // Enable distributed tracing
183
+ "opentelemetry_default_exporter": "console" // Default exporter if OTEL_TRACES_EXPORTER not set
184
+ // Options: "console", "otlp_http"
180
185
  }
181
186
  ```
182
187
 
@@ -188,12 +193,46 @@ The `golf.json` file is the heart of your Golf project configuration. Here's wha
188
193
  - `"streamable-http"` provides HTTP streaming for traditional API clients
189
194
  - `"stdio"` enables integration with command-line tools and scripts
190
195
  - **`host` & `port`**: Control where your server listens. Use `"127.0.0.1"` for local development or `"0.0.0.0"` to accept external connections.
196
+ - **`opentelemetry_enabled`**: When true, enables distributed tracing for debugging and monitoring your MCP server
197
+ - **`opentelemetry_default_exporter`**: Sets the default trace exporter. Can be overridden by the `OTEL_TRACES_EXPORTER` environment variable
198
+
199
+ ## Features
200
+
201
+ ### 🔍 OpenTelemetry Support
202
+
203
+ Golf includes built-in OpenTelemetry instrumentation for distributed tracing. When enabled, it automatically traces:
204
+ - Tool executions with arguments and results
205
+ - Resource reads and template expansions
206
+ - Prompt generations
207
+ - HTTP requests and sessions
208
+
209
+ #### Configuration
210
+
211
+ Enable OpenTelemetry in your `golf.json`:
212
+ ```json
213
+ {
214
+ "opentelemetry_enabled": true,
215
+ "opentelemetry_default_exporter": "otlp_http"
216
+ }
217
+ ```
218
+
219
+ Then configure via environment variables:
220
+ ```bash
221
+ # For OTLP HTTP exporter (e.g., Jaeger, Grafana Tempo)
222
+ OTEL_TRACES_EXPORTER=otlp_http
223
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
224
+ OTEL_SERVICE_NAME=my-golf-server # Optional, defaults to project name
225
+
226
+ # For console exporter (debugging)
227
+ OTEL_TRACES_EXPORTER=console
228
+ ```
229
+
230
+ **Note**: When using the OTLP HTTP exporter, you must set `OTEL_EXPORTER_OTLP_ENDPOINT`. If not configured, Golf will display a warning and disable tracing to avoid errors.
191
231
 
192
232
  ## Roadmap
193
233
 
194
234
  Here are the things we are working hard on:
195
235
 
196
- * **Native OpenTelemetry implementation for tracing**
197
236
  * **`golf deploy` command for one click deployments to Vercel, Blaxel and other providers**
198
237
  * **Production-ready OAuth token management, to allow for persistent, encrypted token storage and client mapping**
199
238