Monitoring System 0.1.0
System resource monitoring with pluggable collectors and alerting
Loading...
Searching...
No Matches
test_trace_exporters.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
5
6#include <gtest/gtest.h>
8// Note: distributed_tracer.h does not exist in include directory
9// #include <kcenon/monitoring/tracing/distributed_tracer.h>
11#include <thread>
12#include <chrono>
13
14using namespace kcenon::monitoring;
15
16class TraceExportersTest : public ::testing::Test {
17protected:
18 void SetUp() override {
19 // Create test spans
21
22 // Create OTEL resource
23 otel_resource_ = create_service_resource("test_service", "1.0.0", "test_namespace");
24 }
25
26 std::vector<trace_span> create_test_spans() {
27 std::vector<trace_span> spans;
28
29 // Root span
30 trace_span root_span;
31 root_span.trace_id = "trace123";
32 root_span.span_id = "span001";
33 root_span.operation_name = "http_request";
34 root_span.service_name = "web_service";
35 root_span.start_time = std::chrono::system_clock::now();
36 root_span.end_time = root_span.start_time + std::chrono::milliseconds(100);
37 root_span.tags["http.method"] = "GET";
38 root_span.tags["http.url"] = "/api/users";
39 root_span.tags["span.kind"] = "server";
40 spans.push_back(root_span);
41
42 // Child span
43 trace_span child_span;
44 child_span.trace_id = "trace123";
45 child_span.span_id = "span002";
46 child_span.parent_span_id = "span001";
47 child_span.operation_name = "database_query";
48 child_span.service_name = "db_service";
49 child_span.start_time = root_span.start_time + std::chrono::milliseconds(10);
50 child_span.end_time = root_span.start_time + std::chrono::milliseconds(80);
51 child_span.tags["db.statement"] = "SELECT * FROM users WHERE id = ?";
52 child_span.tags["db.type"] = "postgresql";
53 child_span.tags["span.kind"] = "client";
54 spans.push_back(child_span);
55
56 return spans;
57 }
58
59 std::vector<trace_span> test_spans_;
61};
62
63TEST_F(TraceExportersTest, TraceExportConfigValidation) {
64 // Valid configuration
65 trace_export_config valid_config;
66 valid_config.endpoint = "http://jaeger:14268/api/traces";
67 valid_config.format = trace_export_format::jaeger_thrift;
68 valid_config.timeout = std::chrono::milliseconds(5000);
69 valid_config.max_batch_size = 100;
70 valid_config.max_queue_size = 1000;
71
72 auto validation = valid_config.validate();
73 EXPECT_TRUE(validation.is_ok());
74
75 // Invalid endpoint
76 trace_export_config invalid_endpoint;
77 invalid_endpoint.endpoint = "";
78 auto endpoint_validation = invalid_endpoint.validate();
79 EXPECT_TRUE(endpoint_validation.is_err());
80 EXPECT_EQ(endpoint_validation.error().code, static_cast<int>(monitoring_error_code::invalid_configuration));
81
82 // Invalid timeout
83 trace_export_config invalid_timeout;
84 invalid_timeout.endpoint = "http://test";
85 invalid_timeout.timeout = std::chrono::milliseconds(0);
86 auto timeout_validation = invalid_timeout.validate();
87 EXPECT_TRUE(timeout_validation.is_err());
88
89 // Invalid batch size
90 trace_export_config invalid_batch;
91 invalid_batch.endpoint = "http://test";
92 invalid_batch.max_batch_size = 0;
93 auto batch_validation = invalid_batch.validate();
94 EXPECT_TRUE(batch_validation.is_err());
95
96 // Invalid queue size
97 trace_export_config invalid_queue;
98 invalid_queue.endpoint = "http://test";
99 invalid_queue.max_batch_size = 100;
100 invalid_queue.max_queue_size = 50;
101 auto queue_validation = invalid_queue.validate();
102 EXPECT_TRUE(queue_validation.is_err());
103}
104
105TEST_F(TraceExportersTest, JaegerSpanConversion) {
106 trace_export_config config;
107 config.endpoint = "http://jaeger:14268/api/traces";
108 config.format = trace_export_format::jaeger_thrift;
109 config.service_name = "test_service";
110
111 jaeger_exporter exporter(config);
112
113 const auto& span = test_spans_[0];
114 auto jaeger_span = exporter.convert_span(span);
115
116 EXPECT_EQ(jaeger_span.trace_id, span.trace_id);
117 EXPECT_EQ(jaeger_span.span_id, span.span_id);
118 EXPECT_EQ(jaeger_span.operation_name, span.operation_name);
119 EXPECT_EQ(jaeger_span.service_name, "test_service"); // Override from config
120
121 // Check tags conversion
122 bool found_http_method = false;
123 bool found_http_url = false;
124 for (const auto& [key, value] : jaeger_span.tags) {
125 if (key == "http.method" && value == "GET") {
126 found_http_method = true;
127 }
128 if (key == "http.url" && value == "/api/users") {
129 found_http_url = true;
130 }
131 }
132 EXPECT_TRUE(found_http_method);
133 EXPECT_TRUE(found_http_url);
134
135 // Check process tags
136 bool found_service_name = false;
137 for (const auto& [key, value] : jaeger_span.process_tags) {
138 if (key == "service.name" && value == "test_service") {
139 found_service_name = true;
140 }
141 }
142 EXPECT_TRUE(found_service_name);
143}
144
145TEST_F(TraceExportersTest, JaegerExporterBasicFunctionality) {
146 trace_export_config config;
147 config.endpoint = "http://jaeger:14268/api/traces";
148 config.format = trace_export_format::jaeger_thrift;
149
150 jaeger_exporter exporter(config);
151
152 // Export spans — stub transport returns error (no real HTTP client)
153 auto export_result = exporter.export_spans(test_spans_);
154 EXPECT_TRUE(export_result.is_err());
155
156 // Check statistics — export failed so failed_exports should increment
157 auto stats = exporter.get_stats();
158 EXPECT_EQ(stats["failed_exports"], 1);
159
160 // Test flush and shutdown (should still work)
161 auto flush_result = exporter.flush();
162 EXPECT_TRUE(flush_result.is_ok());
163
164 auto shutdown_result = exporter.shutdown();
165 EXPECT_TRUE(shutdown_result.is_ok());
166}
167
168TEST_F(TraceExportersTest, ZipkinSpanConversion) {
169 trace_export_config config;
170 config.endpoint = "http://zipkin:9411/api/v2/spans";
171 config.format = trace_export_format::zipkin_json;
172 config.service_name = "test_service";
173
174 zipkin_exporter exporter(config);
175
176 const auto& span = test_spans_[0];
177 auto zipkin_span = exporter.convert_span(span);
178
179 EXPECT_EQ(zipkin_span.trace_id, span.trace_id);
180 EXPECT_EQ(zipkin_span.span_id, span.span_id);
181 EXPECT_EQ(zipkin_span.name, span.operation_name);
182 EXPECT_EQ(zipkin_span.local_endpoint_service_name, "test_service");
183 EXPECT_EQ(zipkin_span.kind, "server"); // From span.kind tag
184
185 // Check tags conversion (span.kind should be excluded)
186 EXPECT_EQ(zipkin_span.tags.count("span.kind"), 0);
187 EXPECT_EQ(zipkin_span.tags.count("http.method"), 1);
188 EXPECT_EQ(zipkin_span.tags["http.method"], "GET");
189}
190
191TEST_F(TraceExportersTest, ZipkinExporterBasicFunctionality) {
192 trace_export_config config;
193 config.endpoint = "http://zipkin:9411/api/v2/spans";
194 config.format = trace_export_format::zipkin_json;
195
196 zipkin_exporter exporter(config);
197
198 // Export spans — stub transport returns error (no real HTTP client)
199 auto export_result = exporter.export_spans(test_spans_);
200 EXPECT_TRUE(export_result.is_err());
201
202 // Check statistics — export failed
203 auto stats = exporter.get_stats();
204 EXPECT_EQ(stats["failed_exports"], 1);
205
206 // Test flush and shutdown (should still work)
207 auto flush_result = exporter.flush();
208 EXPECT_TRUE(flush_result.is_ok());
209
210 auto shutdown_result = exporter.shutdown();
211 EXPECT_TRUE(shutdown_result.is_ok());
212}
213
214TEST_F(TraceExportersTest, OtlpExporterBasicFunctionality) {
215 trace_export_config config;
216 config.endpoint = "http://otlp-collector:4317";
217 config.format = trace_export_format::otlp_grpc;
218
219 otlp_exporter exporter(config, otel_resource_);
220
221 // Export spans
222 auto export_result = exporter.export_spans(test_spans_);
223 EXPECT_TRUE(export_result.is_ok());
224
225 // Check statistics
226 auto stats = exporter.get_stats();
227 EXPECT_EQ(stats["exported_spans"], test_spans_.size());
228 EXPECT_EQ(stats["failed_exports"], 0);
229
230 // Test flush and shutdown
231 auto flush_result = exporter.flush();
232 EXPECT_TRUE(flush_result.is_ok());
233
234 auto shutdown_result = exporter.shutdown();
235 EXPECT_TRUE(shutdown_result.is_ok());
236}
237
238TEST_F(TraceExportersTest, TraceExporterFactory) {
239 // Test Jaeger factory
240 trace_export_config jaeger_config;
241 jaeger_config.endpoint = "http://jaeger:14268";
242 jaeger_config.format = trace_export_format::jaeger_grpc;
243
244 auto jaeger_exporter = trace_exporter_factory::create_exporter(jaeger_config, otel_resource_);
245 EXPECT_TRUE(jaeger_exporter);
246
247 // Test Zipkin factory
248 trace_export_config zipkin_config;
249 zipkin_config.endpoint = "http://zipkin:9411";
250 zipkin_config.format = trace_export_format::zipkin_json;
251
252 auto zipkin_exporter = trace_exporter_factory::create_exporter(zipkin_config, otel_resource_);
253 EXPECT_TRUE(zipkin_exporter);
254
255 // Test OTLP factory
256 trace_export_config otlp_config;
257 otlp_config.endpoint = "http://otlp-collector:4317";
258 otlp_config.format = trace_export_format::otlp_grpc;
259
260 auto otlp_exporter = trace_exporter_factory::create_exporter(otlp_config, otel_resource_);
261 EXPECT_TRUE(otlp_exporter);
262}
263
264TEST_F(TraceExportersTest, SupportedFormatsQuery) {
265 auto jaeger_formats = trace_exporter_factory::get_supported_formats("jaeger");
266 EXPECT_EQ(jaeger_formats.size(), 2);
267 EXPECT_TRUE(std::find(jaeger_formats.begin(), jaeger_formats.end(),
268 trace_export_format::jaeger_thrift) != jaeger_formats.end());
269 EXPECT_TRUE(std::find(jaeger_formats.begin(), jaeger_formats.end(),
270 trace_export_format::jaeger_grpc) != jaeger_formats.end());
271
272 auto zipkin_formats = trace_exporter_factory::get_supported_formats("zipkin");
273 EXPECT_EQ(zipkin_formats.size(), 2);
274 EXPECT_TRUE(std::find(zipkin_formats.begin(), zipkin_formats.end(),
275 trace_export_format::zipkin_json) != zipkin_formats.end());
276 EXPECT_TRUE(std::find(zipkin_formats.begin(), zipkin_formats.end(),
277 trace_export_format::zipkin_protobuf) != zipkin_formats.end());
278
279 auto otlp_formats = trace_exporter_factory::get_supported_formats("otlp");
280 EXPECT_EQ(otlp_formats.size(), 3);
281 EXPECT_TRUE(std::find(otlp_formats.begin(), otlp_formats.end(),
282 trace_export_format::otlp_grpc) != otlp_formats.end());
283
284 auto unknown_formats = trace_exporter_factory::get_supported_formats("unknown");
285 EXPECT_EQ(unknown_formats.size(), 0);
286}
287
288TEST_F(TraceExportersTest, HelperFunctions) {
289 // Test Jaeger helper
290 auto jaeger_exporter = create_jaeger_exporter("http://jaeger:14268",
291 trace_export_format::jaeger_thrift);
292 EXPECT_TRUE(jaeger_exporter);
293
294 // Test Zipkin helper
295 auto zipkin_exporter = create_zipkin_exporter("http://zipkin:9411",
296 trace_export_format::zipkin_protobuf);
297 EXPECT_TRUE(zipkin_exporter);
298
299 // Test OTLP helper
300 auto otlp_exporter = create_otlp_exporter("http://otlp:4317", otel_resource_,
301 trace_export_format::otlp_http_json);
302 EXPECT_TRUE(otlp_exporter);
303}
304
305TEST_F(TraceExportersTest, InvalidFormatHandling) {
306 trace_export_config invalid_jaeger_config;
307 invalid_jaeger_config.endpoint = "http://jaeger:14268";
308 invalid_jaeger_config.format = trace_export_format::zipkin_json; // Wrong format
309
310 jaeger_exporter jaeger_exp(invalid_jaeger_config);
311 auto jaeger_result = jaeger_exp.export_spans(test_spans_);
312 EXPECT_TRUE(jaeger_result.is_err());
313 EXPECT_EQ(jaeger_result.error().code, static_cast<int>(monitoring_error_code::invalid_configuration));
314
315 trace_export_config invalid_zipkin_config;
316 invalid_zipkin_config.endpoint = "http://zipkin:9411";
317 invalid_zipkin_config.format = trace_export_format::jaeger_grpc; // Wrong format
318
319 zipkin_exporter zipkin_exp(invalid_zipkin_config);
320 auto zipkin_result = zipkin_exp.export_spans(test_spans_);
321 EXPECT_TRUE(zipkin_result.is_err());
322 EXPECT_EQ(zipkin_result.error().code, static_cast<int>(monitoring_error_code::invalid_configuration));
323
324 trace_export_config invalid_otlp_config;
325 invalid_otlp_config.endpoint = "http://otlp:4317";
326 invalid_otlp_config.format = trace_export_format::jaeger_thrift; // Wrong format
327
328 otlp_exporter otlp_exp(invalid_otlp_config, otel_resource_);
329 auto otlp_result = otlp_exp.export_spans(test_spans_);
330 EXPECT_TRUE(otlp_result.is_err());
331 EXPECT_EQ(otlp_result.error().code, static_cast<int>(monitoring_error_code::invalid_configuration));
332}
333
334TEST_F(TraceExportersTest, EmptySpansHandling) {
335 std::vector<trace_span> empty_spans;
336
337 trace_export_config config;
338 config.endpoint = "http://test:1234";
339 config.format = trace_export_format::jaeger_grpc;
340
341 jaeger_exporter exporter(config);
342 auto result = exporter.export_spans(empty_spans);
343 // Empty spans may succeed (nothing to send) or fail (stub transport)
344 // Either way, no spans were exported
345 auto stats = exporter.get_stats();
346 EXPECT_EQ(stats["exported_spans"], 0);
347}
348
349TEST_F(TraceExportersTest, LargeSpanBatch) {
350 // Create a large batch of spans
351 std::vector<trace_span> large_batch;
352 for (int i = 0; i < 1000; ++i) {
353 trace_span span;
354 span.trace_id = "trace" + std::to_string(i);
355 span.span_id = "span" + std::to_string(i);
356 span.operation_name = "operation_" + std::to_string(i);
357 span.service_name = "test_service";
358 span.start_time = std::chrono::system_clock::now();
359 span.end_time = span.start_time + std::chrono::milliseconds(1);
360 large_batch.push_back(span);
361 }
362
363 trace_export_config config;
364 config.endpoint = "http://test:1234";
365 config.format = trace_export_format::otlp_grpc;
366 config.max_batch_size = 500; // Smaller than batch size
367
368 otlp_exporter exporter(config, otel_resource_);
369 auto result = exporter.export_spans(large_batch);
370 EXPECT_TRUE(result.is_ok());
371
372 auto stats = exporter.get_stats();
373 EXPECT_EQ(stats["exported_spans"], static_cast<std::size_t>(1000));
374}
375
376// ==============================================================
377// Jaeger / Zipkin Protobuf Round-Trip Tests (Issue #670)
378// ==============================================================
379
382
383TEST(ProtobufWireTest, VarintRoundTrip) {
384 using namespace kcenon::monitoring::protobuf_wire;
385 const std::uint64_t values[] = {
386 0, 1, 127, 128, 255, 16383, 16384,
387 1000000000ULL, (std::uint64_t{1} << 63)
388 };
389 for (std::uint64_t v : values) {
390 std::vector<std::uint8_t> buf;
391 encode_varint(buf, v);
392 reader r(buf.data(), buf.size());
393 auto got = r.read_varint();
394 ASSERT_TRUE(got.has_value()) << "failed for value " << v;
395 EXPECT_EQ(*got, v);
396 EXPECT_TRUE(r.eof());
397 }
398}
399
400TEST(ProtobufWireTest, HexBytesRoundTrip) {
401 using namespace kcenon::monitoring::protobuf_wire;
402 const std::string hex = "0123456789abcdef";
403 auto bytes = hex_to_bytes(hex);
404 EXPECT_EQ(bytes.size(), 8u);
405 EXPECT_EQ(bytes_to_hex(bytes), hex);
406
407 // Invalid hex yields empty
408 EXPECT_TRUE(hex_to_bytes("xyz").empty());
409
410 // Left-padding to 16 bytes
411 auto padded = left_pad(bytes, 16);
412 EXPECT_EQ(padded.size(), 16u);
413 for (std::size_t i = 0; i < 8; ++i) EXPECT_EQ(padded[i], 0u);
414 for (std::size_t i = 0; i < 8; ++i) EXPECT_EQ(padded[8 + i], bytes[i]);
415}
416
417TEST_F(TraceExportersTest, JaegerSpanProtobufRoundTrip) {
418 trace_export_config config;
419 config.endpoint = "http://jaeger:14268";
420 config.format = trace_export_format::jaeger_grpc;
421 config.service_name = "test_service";
422
423 jaeger_exporter exporter(config);
424 auto jaeger_span = exporter.convert_span(test_spans_[0]);
425 auto wire = jaeger_span.to_protobuf();
426 ASSERT_FALSE(wire.empty());
427
430 wire.data(), wire.size(), decoded));
431
432 // Core identifiers: 16-byte trace ID, 8-byte span ID
433 EXPECT_EQ(decoded.trace_id.size(), 16u);
434 EXPECT_EQ(decoded.span_id.size(), 8u);
435 EXPECT_EQ(decoded.operation_name, test_spans_[0].operation_name);
436
437 // Tags preserved (STRING ValueType).
438 bool found_method = false, found_url = false, found_kind = false;
439 for (const auto& kv : decoded.tags) {
440 if (kv.key == "http.method") {
441 EXPECT_EQ(kv.v_str, "GET");
442 EXPECT_EQ(static_cast<int>(kv.v_type), 0);
443 found_method = true;
444 } else if (kv.key == "http.url") {
445 EXPECT_EQ(kv.v_str, "/api/users");
446 found_url = true;
447 } else if (kv.key == "span.kind") {
448 EXPECT_EQ(kv.v_str, "server");
449 found_kind = true;
450 }
451 }
452 EXPECT_TRUE(found_method);
453 EXPECT_TRUE(found_url);
454 EXPECT_TRUE(found_kind);
455
456 // Process service name overridden from config.
457 EXPECT_EQ(decoded.proc.service_name, "test_service");
458}
459
460TEST_F(TraceExportersTest, JaegerSpanProtobufParentReference) {
461 trace_export_config config;
462 config.endpoint = "http://jaeger:14268";
463 config.format = trace_export_format::jaeger_grpc;
464
465 jaeger_exporter exporter(config);
466 // Second test span has parent_span_id set.
467 auto jaeger_span = exporter.convert_span(test_spans_[1]);
468 auto wire = jaeger_span.to_protobuf();
469
472 wire.data(), wire.size(), decoded));
473
474 ASSERT_EQ(decoded.references.size(), 1u);
475 EXPECT_EQ(decoded.references[0].ref_type, 0); // CHILD_OF
476 EXPECT_EQ(decoded.references[0].span_id.size(), 8u);
477}
478
479TEST_F(TraceExportersTest, JaegerBatchProtobufRoundTrip) {
480 trace_export_config config;
481 config.endpoint = "http://jaeger:14268";
482 config.format = trace_export_format::jaeger_grpc;
483 config.service_name = "batch_service";
484
485 jaeger_exporter exporter(config);
486 std::vector<jaeger_span_data> jaeger_spans;
487 for (const auto& s : test_spans_) {
488 jaeger_spans.push_back(exporter.convert_span(s));
489 }
490 auto wire = encode_jaeger_batch(jaeger_spans, "batch_service");
491 ASSERT_FALSE(wire.empty());
492
495 wire.data(), wire.size(), decoded));
496 EXPECT_EQ(decoded.spans.size(), test_spans_.size());
497 EXPECT_TRUE(decoded.has_process);
498 EXPECT_EQ(decoded.proc.service_name, "batch_service");
499}
500
501TEST_F(TraceExportersTest, ZipkinSpanProtobufRoundTrip) {
502 trace_export_config config;
503 config.endpoint = "http://zipkin:9411";
504 config.format = trace_export_format::zipkin_protobuf;
505 config.service_name = "test_service";
506
507 zipkin_exporter exporter(config);
508 auto zipkin_span = exporter.convert_span(test_spans_[0]);
509 auto wire = zipkin_span.to_protobuf();
510 ASSERT_FALSE(wire.empty());
511
514 wire.data(), wire.size(), decoded));
515
516 // Trace ID length: 8 or 16; span ID: 8 bytes.
517 EXPECT_TRUE(decoded.trace_id.size() == 8u || decoded.trace_id.size() == 16u);
518 EXPECT_EQ(decoded.id.size(), 8u);
519 EXPECT_EQ(decoded.name, "http_request");
521 EXPECT_EQ(decoded.local_endpoint.service_name, "test_service");
522
523 // Tags preserved; span.kind excluded by convert_span.
524 bool found_method = false, found_url = false, found_kind = false;
525 for (const auto& [k, v] : decoded.tags) {
526 if (k == "http.method") { EXPECT_EQ(v, "GET"); found_method = true; }
527 if (k == "http.url") { EXPECT_EQ(v, "/api/users"); found_url = true; }
528 if (k == "span.kind") { found_kind = true; }
529 }
530 EXPECT_TRUE(found_method);
531 EXPECT_TRUE(found_url);
532 EXPECT_FALSE(found_kind);
533}
534
535TEST_F(TraceExportersTest, ZipkinSpanProtobufParent) {
536 trace_export_config config;
537 config.endpoint = "http://zipkin:9411";
538 config.format = trace_export_format::zipkin_protobuf;
539
540 zipkin_exporter exporter(config);
541 auto zipkin_span = exporter.convert_span(test_spans_[1]);
542 auto wire = zipkin_span.to_protobuf();
543
546 wire.data(), wire.size(), decoded));
547 EXPECT_EQ(decoded.parent_id.size(), 8u);
549}
550
551TEST_F(TraceExportersTest, ZipkinListOfSpansRoundTrip) {
552 trace_export_config config;
553 config.endpoint = "http://zipkin:9411";
554 config.format = trace_export_format::zipkin_protobuf;
555 config.service_name = "test_service";
556
557 zipkin_exporter exporter(config);
558 std::vector<zipkin_span_data> zipkin_spans;
559 for (const auto& s : test_spans_) {
560 zipkin_spans.push_back(exporter.convert_span(s));
561 }
562 auto wire = encode_zipkin_list_of_spans(zipkin_spans);
563 ASSERT_FALSE(wire.empty());
564
567 wire.data(), wire.size(), decoded));
568 EXPECT_EQ(decoded.spans.size(), test_spans_.size());
569}
570
571TEST(ZipkinProtoTest, SpanKindParsing) {
574 EXPECT_EQ(parse_kind("CLIENT"), span_kind::client);
575 EXPECT_EQ(parse_kind("server"), span_kind::server);
576 EXPECT_EQ(parse_kind("Producer"), span_kind::producer);
577 EXPECT_EQ(parse_kind("CONSUMER"), span_kind::consumer);
578 EXPECT_EQ(parse_kind(""), span_kind::unspecified);
579 EXPECT_EQ(parse_kind("INTERNAL"), span_kind::unspecified);
580}
581
582// ==============================================================
583// OTLP gRPC Exporter Tests
584// ==============================================================
585
587
589protected:
590 void SetUp() override {
592 }
593};
594
595TEST_F(OtlpGrpcExporterTest, ConfigValidation) {
596 // Valid configuration
597 otlp_grpc_config valid_config;
598 valid_config.endpoint = "localhost:4317";
599 valid_config.timeout = std::chrono::milliseconds(5000);
600 valid_config.max_batch_size = 512;
601
602 auto validation = valid_config.validate();
603 EXPECT_TRUE(validation.is_ok());
604
605 // Invalid endpoint (empty)
606 otlp_grpc_config invalid_endpoint;
607 invalid_endpoint.endpoint = "";
608 auto endpoint_validation = invalid_endpoint.validate();
609 EXPECT_TRUE(endpoint_validation.is_err());
610
611 // Invalid timeout (zero)
612 otlp_grpc_config invalid_timeout;
613 invalid_timeout.endpoint = "localhost:4317";
614 invalid_timeout.timeout = std::chrono::milliseconds(0);
615 auto timeout_validation = invalid_timeout.validate();
616 EXPECT_TRUE(timeout_validation.is_err());
617
618 // Invalid batch size (zero)
619 otlp_grpc_config invalid_batch;
620 invalid_batch.endpoint = "localhost:4317";
621 invalid_batch.max_batch_size = 0;
622 auto batch_validation = invalid_batch.validate();
623 EXPECT_TRUE(batch_validation.is_err());
624}
625
626TEST_F(OtlpGrpcExporterTest, SpanConverterBasic) {
627 // Test span conversion to OTLP format
629 test_spans_,
630 "test_service",
631 "1.0.0",
632 {{"environment", "test"}});
633
634 // Verify payload is non-empty
635 EXPECT_GT(payload.size(), 0u);
636
637 // Verify protobuf wire format structure
638 // First byte should be 0x0A (field 1, length-delimited)
639 EXPECT_EQ(payload[0], 0x0A);
640}
641
642TEST_F(OtlpGrpcExporterTest, SpanConverterEmptySpans) {
643 std::vector<trace_span> empty_spans;
644
646 empty_spans,
647 "test_service",
648 "1.0.0",
649 {});
650
651 // Even with no spans, should have resource data
652 EXPECT_GT(payload.size(), 0u);
653}
654
655TEST_F(OtlpGrpcExporterTest, ExporterWithStubTransport) {
656 otlp_grpc_config config;
657 config.endpoint = "localhost:4317";
658 config.service_name = "test_service";
659 config.service_version = "1.0.0";
660
661 // Create exporter with stub transport for testing
662 auto stub_transport = create_stub_grpc_transport();
663 auto* stub_ptr = stub_transport.get();
664
665 otlp_grpc_exporter exporter(config, std::move(stub_transport));
666
667 // Start exporter (connects to stub)
668 auto start_result = exporter.start();
669 EXPECT_TRUE(start_result.is_ok());
670 EXPECT_TRUE(exporter.is_running());
671
672 // Export spans
673 auto export_result = exporter.export_spans(test_spans_);
674 EXPECT_TRUE(export_result.is_ok());
675
676 // Check stats
677 auto stats = exporter.get_stats();
678 EXPECT_EQ(stats["exported_spans"], test_spans_.size());
679 EXPECT_EQ(stats["dropped_spans"], 0u);
680 EXPECT_EQ(stats["failed_exports"], 0u);
681
682 // Check transport statistics
683 auto transport_stats = stub_ptr->get_statistics();
684 EXPECT_EQ(transport_stats.requests_sent, 1u);
685 EXPECT_GT(transport_stats.bytes_sent, 0u);
686
687 // Shutdown
688 auto shutdown_result = exporter.shutdown();
689 EXPECT_TRUE(shutdown_result.is_ok());
690 EXPECT_FALSE(exporter.is_running());
691}
692
693TEST_F(OtlpGrpcExporterTest, ExporterFailedConnection) {
694 otlp_grpc_config config;
695 config.endpoint = "localhost:4317";
696
697 // Create stub transport that simulates failure
698 auto stub_transport = create_stub_grpc_transport();
699 stub_transport->set_simulate_success(false);
700
701 otlp_grpc_exporter exporter(config, std::move(stub_transport));
702
703 // Start should fail due to connection failure
704 auto start_result = exporter.start();
705 EXPECT_TRUE(start_result.is_err());
706}
707
708TEST_F(OtlpGrpcExporterTest, ExporterRetryBehavior) {
709 otlp_grpc_config config;
710 config.endpoint = "localhost:4317";
711 config.max_retry_attempts = 3;
712 config.initial_backoff = std::chrono::milliseconds(10);
713
714 // Track call count
715 int call_count = 0;
716
717 auto stub_transport = create_stub_grpc_transport();
718 stub_transport->set_response_handler([&call_count](const grpc_request&) {
719 call_count++;
720 grpc_response response;
721 if (call_count < 3) {
722 // Simulate UNAVAILABLE error (retryable)
723 response.status_code = 14;
724 response.status_message = "Service unavailable";
725 } else {
726 // Success on third attempt
727 response.status_code = 0;
728 response.status_message = "OK";
729 }
730 return response;
731 });
732
733 otlp_grpc_exporter exporter(config, std::move(stub_transport));
734
735 auto start_result = exporter.start();
736 EXPECT_TRUE(start_result.is_ok());
737
738 auto export_result = exporter.export_spans(test_spans_);
739 EXPECT_TRUE(export_result.is_ok());
740
741 // Should have retried twice before success
742 EXPECT_EQ(call_count, 3);
743
744 auto stats = exporter.get_stats();
745 EXPECT_EQ(stats["retries"], 2u);
746 EXPECT_EQ(stats["exported_spans"], test_spans_.size());
747}
748
749TEST_F(OtlpGrpcExporterTest, ExporterDetailedStats) {
750 otlp_grpc_config config;
751 config.endpoint = "localhost:4317";
752
753 auto stub_transport = create_stub_grpc_transport();
754 // Set response handler to simulate realistic export time
755 stub_transport->set_response_handler([](const grpc_request&) {
756 // Simulate a small delay to ensure measurable export time
757 std::this_thread::sleep_for(std::chrono::microseconds(100));
758 grpc_response response;
759 response.status_code = 0; // OK
760 response.status_message = "OK";
761 return response;
762 });
763 otlp_grpc_exporter exporter(config, std::move(stub_transport));
764
765 auto start_result = exporter.start();
766 EXPECT_TRUE(start_result.is_ok());
767
768 // Export multiple batches
769 for (int i = 0; i < 3; ++i) {
770 auto result = exporter.export_spans(test_spans_);
771 EXPECT_TRUE(result.is_ok());
772 }
773
774 auto detailed_stats = exporter.get_detailed_stats();
775 EXPECT_EQ(detailed_stats.spans_exported, test_spans_.size() * 3);
776 EXPECT_EQ(detailed_stats.spans_dropped, 0u);
777 EXPECT_EQ(detailed_stats.export_failures, 0u);
778 EXPECT_GT(detailed_stats.total_export_time.count(), 0);
779}
780
781TEST_F(OtlpGrpcExporterTest, FactoryFunctions) {
782 // Test default factory
783 auto exporter1 = create_otlp_grpc_exporter();
784 EXPECT_TRUE(exporter1 != nullptr);
785 EXPECT_EQ(exporter1->config().endpoint, "localhost:4317");
786
787 // Test factory with endpoint
788 auto exporter2 = create_otlp_grpc_exporter("collector:4317");
789 EXPECT_TRUE(exporter2 != nullptr);
790 EXPECT_EQ(exporter2->config().endpoint, "collector:4317");
791
792 // Test factory with config
793 otlp_grpc_config config;
794 config.endpoint = "otlp.example.com:443";
795 config.use_tls = true;
796 config.service_name = "my_service";
797
798 auto exporter3 = create_otlp_grpc_exporter(config);
799 EXPECT_TRUE(exporter3 != nullptr);
800 EXPECT_EQ(exporter3->config().endpoint, "otlp.example.com:443");
801 EXPECT_TRUE(exporter3->config().use_tls);
802 EXPECT_EQ(exporter3->config().service_name, "my_service");
803}
804
805TEST_F(OtlpGrpcExporterTest, ExportEmptySpans) {
806 otlp_grpc_config config;
807 config.endpoint = "localhost:4317";
808
809 auto stub_transport = create_stub_grpc_transport();
810 otlp_grpc_exporter exporter(config, std::move(stub_transport));
811
812 auto start_result = exporter.start();
813 EXPECT_TRUE(start_result.is_ok());
814
815 // Export empty vector should succeed without sending
816 std::vector<trace_span> empty_spans;
817 auto export_result = exporter.export_spans(empty_spans);
818 EXPECT_TRUE(export_result.is_ok());
819
820 auto stats = exporter.get_stats();
821 EXPECT_EQ(stats["exported_spans"], 0u);
822}
std::vector< trace_span > create_test_spans()
std::vector< trace_span > test_spans_
Jaeger trace exporter implementation.
std::unordered_map< std::string, std::size_t > get_stats() const override
Get exporter statistics.
common::VoidResult flush() override
Flush any pending spans.
jaeger_span_data convert_span(const trace_span &span) const
Convert internal span to Jaeger format.
common::VoidResult shutdown() override
Shutdown the exporter.
common::VoidResult export_spans(const std::vector< trace_span > &spans) override
Export a batch of spans.
OpenTelemetry Protocol (OTLP) trace exporter implementation.
common::VoidResult shutdown() override
Shutdown the exporter.
common::VoidResult flush() override
Flush any pending spans.
common::VoidResult export_spans(const std::vector< trace_span > &spans) override
Export a batch of spans.
std::unordered_map< std::string, std::size_t > get_stats() const override
Get exporter statistics.
common::VoidResult shutdown() override
Shutdown the exporter.
otlp_exporter_stats get_detailed_stats() const
Get detailed statistics.
common::VoidResult start()
Start the exporter.
bool is_running() const
Check if exporter is running.
std::unordered_map< std::string, std::size_t > get_stats() const override
Get exporter statistics.
common::VoidResult export_spans(const std::vector< trace_span > &spans) override
Export a batch of spans.
static std::vector< uint8_t > convert_to_otlp(const std::vector< trace_span > &spans, const std::string &service_name, const std::string &service_version, const std::unordered_map< std::string, std::string > &resource_attributes)
Convert spans to OTLP protobuf format.
Minimal protobuf wire reader used for round-trip tests.
std::optional< std::uint64_t > read_varint()
static std::vector< trace_export_format > get_supported_formats(const std::string &backend)
Get supported formats for a specific backend.
static std::unique_ptr< trace_exporter_interface > create_exporter(const trace_export_config &config, const otel_resource &resource=create_service_resource("monitoring_system", "2.0.0"))
Create a trace exporter based on format.
Zipkin trace exporter implementation.
common::VoidResult export_spans(const std::vector< trace_span > &spans) override
Export a batch of spans.
std::unordered_map< std::string, std::size_t > get_stats() const override
Get exporter statistics.
common::VoidResult shutdown() override
Shutdown the exporter.
common::VoidResult flush() override
Flush any pending spans.
zipkin_span_data convert_span(const trace_span &span) const
Convert internal span to Zipkin format.
Serialization/deserialization of Jaeger api_v2 model.proto messages.
bool decode_span(const std::uint8_t *data, std::size_t size, span &out)
bool decode_batch(const std::uint8_t *data, std::size_t size, batch &out)
bool decode_span(const std::uint8_t *data, std::size_t size, span &out)
span_kind parse_kind(const std::string &value)
Convert a textual Zipkin kind (e.g. "CLIENT") to its enum value.
bool decode_list_of_spans(const std::uint8_t *data, std::size_t size, list_of_spans &out)
std::unique_ptr< stub_grpc_transport > create_stub_grpc_transport()
Create stub gRPC transport for testing.
std::vector< uint8_t > encode_zipkin_list_of_spans(const std::vector< zipkin_span_data > &spans)
Encode a batch of Zipkin spans into a ListOfSpans protobuf message suitable for POST /api/v2/spans wi...
std::unique_ptr< zipkin_exporter > create_zipkin_exporter(const std::string &endpoint, trace_export_format format=trace_export_format::zipkin_json)
Helper function to create a Zipkin exporter.
std::unique_ptr< otlp_grpc_exporter > create_otlp_grpc_exporter(const std::string &endpoint="localhost:4317")
Create OTLP gRPC exporter with default configuration.
otel_resource create_service_resource(const std::string &service_name, const std::string &service_version="1.0.0", const std::string &service_namespace="")
Create OpenTelemetry resource with service information.
std::unique_ptr< otlp_exporter > create_otlp_exporter(const std::string &endpoint, const otel_resource &resource, trace_export_format format=trace_export_format::otlp_grpc)
Helper function to create an OTLP exporter.
std::vector< uint8_t > encode_jaeger_batch(const std::vector< jaeger_span_data > &spans, const std::string &service_name)
Encode a batch of Jaeger spans (with shared process) into a Jaeger api_v2 Batch protobuf message.
std::unique_ptr< jaeger_exporter > create_jaeger_exporter(const std::string &endpoint, trace_export_format format=trace_export_format::jaeger_grpc)
Helper function to create a Jaeger exporter.
OpenTelemetry compatibility layer for monitoring system integration.
OTLP gRPC trace exporter implementation.
gRPC request configuration
std::vector< span_ref > references
std::vector< std::uint8_t > trace_id
std::vector< std::uint8_t > span_id
std::vector< uint8_t > to_protobuf() const
Convert this span to a Jaeger api_v2 Span protobuf message.
OpenTelemetry resource representation.
Configuration for OTLP gRPC exporter.
std::string service_version
Service version.
std::string endpoint
OTLP receiver endpoint.
std::size_t max_retry_attempts
Maximum retry attempts.
common::VoidResult validate() const
Validate configuration.
std::size_t max_batch_size
Maximum spans per batch.
std::chrono::milliseconds initial_backoff
Initial retry backoff.
std::chrono::milliseconds timeout
Request timeout.
Configuration for trace exporters.
std::chrono::milliseconds timeout
Request timeout.
std::size_t max_queue_size
Maximum queued spans.
std::optional< std::string > service_name
Override service name.
common::VoidResult validate() const
Validate export configuration.
std::size_t max_batch_size
Maximum spans per batch.
Trace span representing a unit of work in distributed tracing.
std::unordered_map< std::string, std::string > tags
std::chrono::system_clock::time_point end_time
std::chrono::system_clock::time_point start_time
std::vector< std::uint8_t > trace_id
std::vector< std::uint8_t > id
std::vector< std::uint8_t > parent_id
std::vector< std::pair< std::string, std::string > > tags
std::vector< uint8_t > to_protobuf() const
Convert this span to a Zipkin Span protobuf message.
TEST(ProtobufWireTest, VarintRoundTrip)
TEST_F(TraceExportersTest, TraceExportConfigValidation)
Trace data exporters for various distributed tracing systems.
Serialization/deserialization of Zipkin zipkin.proto messages.