Skip to content

Commit 7056457

Browse files
committed
Add telemetry spans and trace propagation
Add telemetry helpers, client/server span instrumentation, and tests for trace context propagation.
1 parent 8de5788 commit 7056457

7 files changed

Lines changed: 877 additions & 26 deletions

File tree

CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ add_library(fastmcpp_core STATIC
4040
src/client/client.cpp
4141
src/client/sampling_handlers.cpp
4242
src/client/transports.cpp
43+
src/telemetry.cpp
4344
src/util/json_schema.cpp
4445
src/util/json_schema_type.cpp
4546
src/settings.cpp
@@ -446,6 +447,10 @@ if(FASTMCPP_BUILD_TESTS)
446447
target_link_libraries(fastmcpp_server_middleware_pipeline PRIVATE fastmcpp_core)
447448
add_test(NAME fastmcpp_server_middleware_pipeline COMMAND fastmcpp_server_middleware_pipeline)
448449

450+
add_executable(fastmcpp_telemetry tests/telemetry/tracing.cpp)
451+
target_link_libraries(fastmcpp_telemetry PRIVATE fastmcpp_core)
452+
add_test(NAME fastmcpp_telemetry COMMAND fastmcpp_telemetry)
453+
449454
add_executable(fastmcpp_stdio_client tests/transports/stdio_client.cpp)
450455
target_link_libraries(fastmcpp_stdio_client PRIVATE fastmcpp_core)
451456
add_test(NAME fastmcpp_stdio_client COMMAND fastmcpp_stdio_client)

include/fastmcpp/client/client.hpp

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "fastmcpp/client/types.hpp"
88
#include "fastmcpp/exceptions.hpp"
99
#include "fastmcpp/server/server.hpp"
10+
#include "fastmcpp/telemetry.hpp"
1011
#include "fastmcpp/types.hpp"
1112
#include "fastmcpp/util/json_schema.hpp"
1213
#include "fastmcpp/util/json_schema_type.hpp"
@@ -68,6 +69,15 @@ class IServerRequestTransport
6869
virtual void set_server_request_handler(ServerRequestHandler handler) = 0;
6970
};
7071

72+
/// Optional transport interface: some transports expose MCP session IDs.
73+
class ISessionTransport
74+
{
75+
public:
76+
virtual ~ISessionTransport() = default;
77+
virtual std::string session_id() const = 0;
78+
virtual bool has_session() const = 0;
79+
};
80+
7181
/// Loopback transport for in-process server testing
7282
class LoopbackTransport : public ITransport
7383
{
@@ -240,12 +250,15 @@ class Client
240250
CallToolResult call_tool_mcp(const std::string& name, const fastmcpp::Json& arguments,
241251
const CallToolOptions& options = CallToolOptions{})
242252
{
253+
auto span =
254+
telemetry::client_span("tool " + name, "tools/call", name, transport_session_id());
243255

244256
fastmcpp::Json payload = {{"name", name}, {"arguments", arguments}};
245257

246258
// Add _meta if provided
247-
if (options.meta)
248-
payload["_meta"] = *options.meta;
259+
auto propagated_meta = telemetry::inject_trace_context(options.meta);
260+
if (propagated_meta)
261+
payload["_meta"] = *propagated_meta;
249262

250263
if (options.progress_handler)
251264
options.progress_handler(0.0f, std::nullopt, "request started");
@@ -439,7 +452,13 @@ class Client
439452
/// Read a resource by URI
440453
ReadResourceResult read_resource_mcp(const std::string& uri)
441454
{
442-
auto response = call("resources/read", {{"uri", uri}});
455+
auto span = telemetry::client_span("resource " + uri, "resources/read", uri,
456+
transport_session_id());
457+
fastmcpp::Json payload = {{"uri", uri}};
458+
auto propagated_meta = telemetry::inject_trace_context(std::nullopt);
459+
if (propagated_meta)
460+
payload["_meta"] = *propagated_meta;
461+
auto response = call("resources/read", payload);
443462
return parse_read_resource_result(response);
444463
}
445464

@@ -477,7 +496,8 @@ class Client
477496
GetPromptResult get_prompt_mcp(const std::string& name,
478497
const fastmcpp::Json& arguments = fastmcpp::Json::object())
479498
{
480-
499+
auto span =
500+
telemetry::client_span("prompt " + name, "prompts/get", name, transport_session_id());
481501
fastmcpp::Json payload = {{"name", name}};
482502
if (!arguments.empty())
483503
{
@@ -491,6 +511,10 @@ class Client
491511
payload["arguments"] = stringArgs;
492512
}
493513

514+
auto propagated_meta = telemetry::inject_trace_context(std::nullopt);
515+
if (propagated_meta)
516+
payload["_meta"] = *propagated_meta;
517+
494518
auto response = call("prompts/get", payload);
495519
return parse_get_prompt_result(response);
496520
}
@@ -810,6 +834,18 @@ class Client
810834
}
811835
}
812836

837+
std::optional<std::string> transport_session_id() const
838+
{
839+
if (!transport_)
840+
return std::nullopt;
841+
if (auto* session_transport = dynamic_cast<ISessionTransport*>(transport_.get()))
842+
{
843+
if (session_transport->has_session())
844+
return session_transport->session_id();
845+
}
846+
return std::nullopt;
847+
}
848+
813849
// Internal constructor for cloning
814850
Client(std::shared_ptr<ITransport> t, std::shared_ptr<CallbackState> callbacks,
815851
bool /*internal*/)
@@ -1541,6 +1577,9 @@ inline std::shared_ptr<ResourceTask> Client::read_resource_task(const std::strin
15411577

15421578
fastmcpp::Json task_meta = {{"ttl", ttl_ms}};
15431579
payload["_meta"] = fastmcpp::Json{{"modelcontextprotocol.io/task", std::move(task_meta)}};
1580+
auto propagated_meta = telemetry::inject_trace_context(payload["_meta"]);
1581+
if (propagated_meta)
1582+
payload["_meta"] = *propagated_meta;
15441583

15451584
auto response = call("resources/read", payload);
15461585

@@ -1575,6 +1614,9 @@ Client::get_prompt_task(const std::string& name, const fastmcpp::Json& arguments
15751614

15761615
fastmcpp::Json task_meta = {{"ttl", ttl_ms}};
15771616
payload["_meta"] = fastmcpp::Json{{"modelcontextprotocol.io/task", std::move(task_meta)}};
1617+
auto propagated_meta = telemetry::inject_trace_context(payload["_meta"]);
1618+
if (propagated_meta)
1619+
payload["_meta"] = *propagated_meta;
15781620

15791621
auto response = call("prompts/get", payload);
15801622

include/fastmcpp/client/transports.hpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ class StdioTransport : public ITransport
114114
/// 3. Server sends JSON-RPC responses back via the SSE stream
115115
class SseClientTransport : public ITransport,
116116
public IServerRequestTransport,
117-
public IResettableTransport
117+
public IResettableTransport,
118+
public ISessionTransport
118119
{
119120
public:
120121
/// Construct an SSE client transport
@@ -179,7 +180,9 @@ class SseClientTransport : public ITransport,
179180
/// 3. Session ID management via Mcp-Session-Id header
180181
///
181182
/// Reference: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/transports/
182-
class StreamableHttpTransport : public ITransport, public IResettableTransport
183+
class StreamableHttpTransport : public ITransport,
184+
public IResettableTransport,
185+
public ISessionTransport
183186
{
184187
public:
185188
/// Construct a Streamable HTTP client transport

include/fastmcpp/telemetry.hpp

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// fastmcpp OpenTelemetry-style tracing helpers (no-op unless exporter configured)
2+
#pragma once
3+
4+
#include "fastmcpp/types.hpp"
5+
6+
#include <memory>
7+
#include <optional>
8+
#include <string>
9+
#include <unordered_map>
10+
#include <vector>
11+
12+
namespace fastmcpp::telemetry
13+
{
14+
15+
constexpr const char* INSTRUMENTATION_NAME = "fastmcp";
16+
constexpr const char* TRACE_PARENT_KEY = "fastmcp.traceparent";
17+
constexpr const char* TRACE_STATE_KEY = "fastmcp.tracestate";
18+
19+
struct SpanContext
20+
{
21+
std::string trace_id;
22+
std::string span_id;
23+
24+
bool is_valid() const
25+
{
26+
return trace_id.size() == 32 && span_id.size() == 16;
27+
}
28+
};
29+
30+
enum class SpanKind
31+
{
32+
Internal,
33+
Client,
34+
Server
35+
};
36+
37+
enum class StatusCode
38+
{
39+
Unset,
40+
Ok,
41+
Error
42+
};
43+
44+
struct Span
45+
{
46+
std::string name;
47+
std::string instrumentation_name;
48+
std::optional<std::string> instrumentation_version;
49+
SpanKind kind{SpanKind::Internal};
50+
SpanContext context{};
51+
std::optional<SpanContext> parent;
52+
StatusCode status{StatusCode::Unset};
53+
std::unordered_map<std::string, fastmcpp::Json> attributes;
54+
std::optional<std::string> exception_message;
55+
56+
void set_attribute(const std::string& key, const fastmcpp::Json& value)
57+
{
58+
attributes[key] = value;
59+
}
60+
61+
void set_attributes(const std::unordered_map<std::string, fastmcpp::Json>& attrs)
62+
{
63+
attributes.insert(attrs.begin(), attrs.end());
64+
}
65+
66+
void record_exception(const std::string& message)
67+
{
68+
exception_message = message;
69+
status = StatusCode::Error;
70+
}
71+
72+
void set_status(StatusCode code)
73+
{
74+
status = code;
75+
}
76+
};
77+
78+
class SpanExporter
79+
{
80+
public:
81+
virtual ~SpanExporter() = default;
82+
virtual void export_span(const Span& span) = 0;
83+
};
84+
85+
class InMemorySpanExporter : public SpanExporter
86+
{
87+
public:
88+
void export_span(const Span& span) override;
89+
const std::vector<Span>& finished_spans() const;
90+
void reset();
91+
92+
private:
93+
std::vector<Span> spans_;
94+
};
95+
96+
class SpanScope
97+
{
98+
public:
99+
SpanScope() = default;
100+
explicit SpanScope(Span span, bool active);
101+
SpanScope(const SpanScope&) = delete;
102+
SpanScope& operator=(const SpanScope&) = delete;
103+
SpanScope(SpanScope&& other) noexcept;
104+
SpanScope& operator=(SpanScope&& other) noexcept;
105+
~SpanScope();
106+
107+
Span& span();
108+
bool active() const;
109+
void end();
110+
111+
private:
112+
void finalize(bool record_error);
113+
114+
bool active_{false};
115+
bool ended_{false};
116+
int uncaught_on_enter_{0};
117+
Span span_;
118+
};
119+
120+
class Tracer
121+
{
122+
public:
123+
explicit Tracer(std::string instrumentation_name, std::optional<std::string> version)
124+
: instrumentation_name_(std::move(instrumentation_name)), version_(std::move(version))
125+
{
126+
}
127+
128+
SpanScope start_span(const std::string& name, SpanKind kind,
129+
const std::optional<SpanContext>& parent = std::nullopt) const;
130+
131+
private:
132+
std::string instrumentation_name_;
133+
std::optional<std::string> version_;
134+
};
135+
136+
Tracer get_tracer(std::optional<std::string> version = std::nullopt);
137+
void set_span_exporter(std::shared_ptr<SpanExporter> exporter);
138+
std::shared_ptr<SpanExporter> span_exporter();
139+
SpanContext current_span_context();
140+
141+
std::optional<fastmcpp::Json> inject_trace_context(const std::optional<fastmcpp::Json>& meta);
142+
SpanContext extract_trace_context(const std::optional<fastmcpp::Json>& meta);
143+
144+
SpanScope client_span(const std::string& name, const std::string& method,
145+
const std::string& component_key,
146+
const std::optional<std::string>& session_id = std::nullopt);
147+
SpanScope server_span(const std::string& name, const std::string& method,
148+
const std::string& server_name, const std::string& component_type,
149+
const std::string& component_key,
150+
const std::optional<fastmcpp::Json>& request_meta,
151+
const std::optional<std::string>& session_id = std::nullopt);
152+
SpanScope delegate_span(const std::string& name, const std::string& provider_type,
153+
const std::string& component_key);
154+
155+
} // namespace fastmcpp::telemetry

0 commit comments

Comments
 (0)