Monitoring System 0.1.0
System resource monitoring with pluggable collectors and alerting
Loading...
Searching...
No Matches
test_metric_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>
14#include <thread>
15#include <chrono>
16
17using namespace kcenon::monitoring;
18
19class MetricExportersTest : public ::testing::Test {
20protected:
21 void SetUp() override {
22 // Create test monitoring data
24
25 // Create test metrics snapshot
27
28 // Create OTEL resource
29 otel_resource_ = create_service_resource("test_service", "1.0.0", "test_namespace");
30 }
31
33 monitoring_data data("web_server");
34 data.add_metric("http_requests_total", 1500.0);
35 data.add_metric("http_request_duration_seconds", 0.250);
36 data.add_metric("memory_usage_bytes", 1024000.0);
37 data.add_metric("cpu_usage_percent", 75.5);
38
39 data.add_tag("environment", "production");
40 data.add_tag("region", "us-west-2");
41 data.add_tag("version", "1.2.3");
42
43 return data;
44 }
45
47 metrics_snapshot snapshot;
48 snapshot.source_id = "system_monitor";
49 snapshot.capture_time = std::chrono::system_clock::now();
50
51 snapshot.add_metric("system_load_1m", 2.1);
52 snapshot.add_metric("system_load_5m", 1.8);
53 snapshot.add_metric("disk_usage_percent", 68.3);
54 snapshot.add_metric("network_bytes_in", 987654.0);
55 snapshot.add_metric("network_bytes_out", 654321.0);
56
57 // Add tags to metrics
58 snapshot.metrics[0].tags["host"] = "server01";
59 snapshot.metrics[1].tags["host"] = "server01";
60 snapshot.metrics[2].tags["mount"] = "/var";
61 snapshot.metrics[3].tags["interface"] = "eth0";
62 snapshot.metrics[4].tags["interface"] = "eth0";
63
64 return snapshot;
65 }
66
70};
71
72TEST_F(MetricExportersTest, MetricExportConfigValidation) {
73 // Valid configuration
74 metric_export_config valid_config;
75 valid_config.endpoint = "http://prometheus:9090";
76 valid_config.format = metric_export_format::prometheus_text;
77 valid_config.push_interval = std::chrono::milliseconds(15000);
78 valid_config.max_batch_size = 1000;
79 valid_config.max_queue_size = 10000;
80
81 auto validation = valid_config.validate();
82 EXPECT_TRUE(validation.is_ok());
83
84 // Valid configuration with port
85 metric_export_config port_config;
86 port_config.port = 8125;
87 port_config.format = metric_export_format::statsd_plain;
88 auto port_validation = port_config.validate();
89 EXPECT_TRUE(port_validation.is_ok());
90
91 // Invalid configuration (no endpoint or port)
92 metric_export_config invalid_config;
93 invalid_config.format = metric_export_format::prometheus_text;
94 auto invalid_validation = invalid_config.validate();
95 EXPECT_TRUE(invalid_validation.is_err());
96
97 // Invalid push interval
99 invalid_interval.endpoint = "http://test";
100 invalid_interval.push_interval = std::chrono::milliseconds(0);
101 auto interval_validation = invalid_interval.validate();
102 EXPECT_TRUE(interval_validation.is_err());
103
104 // Invalid batch size
105 metric_export_config invalid_batch;
106 invalid_batch.endpoint = "http://test";
107 invalid_batch.max_batch_size = 0;
108 auto batch_validation = invalid_batch.validate();
109 EXPECT_FALSE(batch_validation.is_ok());
110
111 // Invalid queue size
112 metric_export_config invalid_queue;
113 invalid_queue.endpoint = "http://test";
114 invalid_queue.max_batch_size = 1000;
115 invalid_queue.max_queue_size = 500;
116 auto queue_validation = invalid_queue.validate();
117 EXPECT_FALSE(queue_validation.is_ok());
118}
119
120TEST_F(MetricExportersTest, PrometheusMetricConversion) {
122 config.endpoint = "http://prometheus:9090";
123 config.format = metric_export_format::prometheus_text;
124 config.instance_id = "test_instance";
125 config.labels["datacenter"] = "dc1";
126
127 prometheus_exporter exporter(config);
128
129 // Test monitoring_data conversion
130 auto prom_metrics = exporter.convert_monitoring_data(test_data_);
131 EXPECT_EQ(prom_metrics.size(), 4); // 4 metrics in test data
132
133 // Find http_requests_total metric
134 auto it = std::find_if(prom_metrics.begin(), prom_metrics.end(),
135 [](const prometheus_metric_data& m) { return m.name == "http_requests_total"; });
136 ASSERT_NE(it, prom_metrics.end());
137
138 const auto& requests_metric = *it;
139 EXPECT_EQ(requests_metric.name, "http_requests_total");
140 EXPECT_EQ(requests_metric.type, metric_type::counter);
141 EXPECT_EQ(requests_metric.value, 1500.0);
142 EXPECT_EQ(requests_metric.labels.at("component"), "web_server");
143 EXPECT_EQ(requests_metric.labels.at("environment"), "production");
144 EXPECT_EQ(requests_metric.labels.at("datacenter"), "dc1");
145 EXPECT_EQ(requests_metric.labels.at("instance"), "test_instance");
146
147 // Test snapshot conversion
148 auto snapshot_metrics = exporter.convert_snapshot(test_snapshot_);
149 EXPECT_EQ(snapshot_metrics.size(), 5); // 5 metrics in test snapshot
150
151 // Check snapshot metric
152 const auto& load_metric = snapshot_metrics[0];
153 EXPECT_EQ(load_metric.name, "system_load_1m");
154 EXPECT_EQ(load_metric.type, metric_type::gauge);
155 EXPECT_EQ(load_metric.value, 2.1);
156 EXPECT_EQ(load_metric.labels.at("source"), "system_monitor");
157 EXPECT_EQ(load_metric.labels.at("host"), "server01");
158}
159
160TEST_F(MetricExportersTest, PrometheusTextFormat) {
162 metric.name = "http_requests_total";
163 metric.type = metric_type::counter;
164 metric.value = 1500.0;
165 metric.help_text = "Total number of HTTP requests";
166 metric.labels["method"] = "GET";
167 metric.labels["status"] = "200";
168 metric.timestamp = std::chrono::system_clock::from_time_t(1640995200); // Fixed timestamp: 2022-01-01 00:00:00 UTC
169
170 std::string prometheus_text = metric.to_prometheus_text();
171
172 // Check format components
173 EXPECT_NE(prometheus_text.find("# HELP http_requests_total Total number of HTTP requests"), std::string::npos);
174 EXPECT_NE(prometheus_text.find("# TYPE http_requests_total counter"), std::string::npos);
175 EXPECT_NE(prometheus_text.find("http_requests_total{"), std::string::npos);
176 EXPECT_NE(prometheus_text.find("method=\"GET\""), std::string::npos);
177 EXPECT_NE(prometheus_text.find("status=\"200\""), std::string::npos);
178 EXPECT_NE(prometheus_text.find("} 1500"), std::string::npos);
179}
180
181TEST_F(MetricExportersTest, PrometheusExporterBasicFunctionality) {
183 config.endpoint = "http://prometheus:9090";
184 config.format = metric_export_format::prometheus_text;
185
186 prometheus_exporter exporter(config);
187
188 // Export monitoring data
189 std::vector<monitoring_data> data_batch = {test_data_};
190 auto export_result = exporter.export_metrics(data_batch);
191 EXPECT_TRUE(export_result.is_ok());
192
193 // Export snapshot
194 auto snapshot_result = exporter.export_snapshot(test_snapshot_);
195 EXPECT_TRUE(snapshot_result.is_ok());
196
197 // Get metrics text
198 std::string metrics_text = exporter.get_metrics_text();
199 EXPECT_FALSE(metrics_text.empty());
200 EXPECT_NE(metrics_text.find("http_requests_total"), std::string::npos);
201 EXPECT_NE(metrics_text.find("system_load_1m"), std::string::npos);
202
203 // Check statistics
204 auto stats = exporter.get_stats();
205 EXPECT_EQ(stats["exported_metrics"], 2); // 1 data + 1 snapshot
206 EXPECT_EQ(stats["failed_exports"], 0);
207 EXPECT_EQ(stats["scrape_requests"], 1); // Called get_metrics_text once
208
209 // Test flush and shutdown
210 auto flush_result = exporter.flush();
211 EXPECT_TRUE(flush_result.is_ok());
212
213 auto shutdown_result = exporter.shutdown();
214 EXPECT_TRUE(shutdown_result.is_ok());
215}
216
217TEST_F(MetricExportersTest, StatsDMetricConversion) {
219 config.endpoint = "statsd.example.com";
220 config.port = 8125;
221 config.format = metric_export_format::statsd_datadog;
222 config.instance_id = "test_instance";
223 config.labels["datacenter"] = "dc1";
224
225 statsd_exporter exporter(config);
226
227 // Test monitoring_data conversion
228 auto statsd_metrics = exporter.convert_monitoring_data(test_data_);
229 EXPECT_EQ(statsd_metrics.size(), 4); // 4 metrics in test data
230
231 // Find http_requests_total metric
232 auto it = std::find_if(statsd_metrics.begin(), statsd_metrics.end(),
233 [](const statsd_metric_data& m) { return m.name == "http_requests_total"; });
234 ASSERT_NE(it, statsd_metrics.end());
235
236 const auto& requests_metric = *it;
237 EXPECT_EQ(requests_metric.name, "http_requests_total");
238 EXPECT_EQ(requests_metric.type, metric_type::counter);
239 EXPECT_EQ(requests_metric.value, 1500.0);
240 EXPECT_EQ(requests_metric.sample_rate, 1.0);
241 EXPECT_EQ(requests_metric.tags.at("component"), "web_server");
242 EXPECT_EQ(requests_metric.tags.at("environment"), "production");
243 EXPECT_EQ(requests_metric.tags.at("datacenter"), "dc1");
244
245 // Test snapshot conversion
246 auto snapshot_metrics = exporter.convert_snapshot(test_snapshot_);
247 EXPECT_EQ(snapshot_metrics.size(), 5); // 5 metrics in test snapshot
248}
249
250TEST_F(MetricExportersTest, StatsDTextFormat) {
251 statsd_metric_data counter_metric;
252 counter_metric.name = "http_requests_total";
253 counter_metric.type = metric_type::counter;
254 counter_metric.value = 1500.0;
255 counter_metric.sample_rate = 1.0;
256 counter_metric.tags["method"] = "GET";
257 counter_metric.tags["status"] = "200";
258
259 // Test plain StatsD format
260 std::string plain_statsd = counter_metric.to_statsd_format(false);
261 EXPECT_EQ(plain_statsd, "http_requests_total:1500|c");
262
263 // Test DataDog format with tags
264 std::string datadog_statsd = counter_metric.to_statsd_format(true);
265 EXPECT_NE(datadog_statsd.find("http_requests_total:1500|c|#"), std::string::npos);
266 EXPECT_NE(datadog_statsd.find("method:GET"), std::string::npos);
267 EXPECT_NE(datadog_statsd.find("status:200"), std::string::npos);
268
269 // Test timer metric
270 statsd_metric_data timer_metric;
271 timer_metric.name = "request_duration";
272 timer_metric.type = metric_type::timer;
273 timer_metric.value = 250.0;
274 timer_metric.sample_rate = 0.1;
275
276 std::string timer_statsd = timer_metric.to_statsd_format(false);
277 EXPECT_EQ(timer_statsd, "request_duration:250|ms|@0.1");
278}
279
280TEST_F(MetricExportersTest, StatsDExporterBasicFunctionality) {
282 config.endpoint = "statsd.example.com";
283 config.port = 8125;
284 config.format = metric_export_format::statsd_plain;
285
286 statsd_exporter exporter(config);
287
288 // Export monitoring data
289 std::vector<monitoring_data> data_batch = {test_data_};
290 auto export_result = exporter.export_metrics(data_batch);
291 EXPECT_TRUE(export_result.is_ok());
292
293 // Export snapshot
294 auto snapshot_result = exporter.export_snapshot(test_snapshot_);
295 EXPECT_TRUE(snapshot_result.is_ok());
296
297 // Check statistics
298 auto stats = exporter.get_stats();
299 EXPECT_EQ(stats["exported_metrics"], 2); // 1 data + 1 snapshot
300 EXPECT_EQ(stats["failed_exports"], 0);
301 EXPECT_EQ(stats["sent_packets"], 2); // 2 UDP packets sent
302
303 // Test flush and shutdown
304 auto flush_result = exporter.flush();
305 EXPECT_TRUE(flush_result.is_ok());
306
307 auto shutdown_result = exporter.shutdown();
308 EXPECT_TRUE(shutdown_result.is_ok());
309}
310
311TEST_F(MetricExportersTest, OtlpMetricsExporterBasicFunctionality) {
313 config.endpoint = "http://otlp-collector:4317";
314 config.format = metric_export_format::otlp_grpc;
315
316 otlp_metrics_exporter exporter(config, otel_resource_);
317
318 // Export monitoring data
319 std::vector<monitoring_data> data_batch = {test_data_};
320 auto export_result = exporter.export_metrics(data_batch);
321 EXPECT_TRUE(export_result.is_ok());
322
323 // Export snapshot
324 auto snapshot_result = exporter.export_snapshot(test_snapshot_);
325 EXPECT_TRUE(snapshot_result.is_ok());
326
327 // Check statistics
328 auto stats = exporter.get_stats();
329 EXPECT_EQ(stats["exported_metrics"], 2); // 1 data + 1 snapshot
330 EXPECT_EQ(stats["failed_exports"], 0);
331
332 // Test flush and shutdown
333 auto flush_result = exporter.flush();
334 EXPECT_TRUE(flush_result.is_ok());
335
336 auto shutdown_result = exporter.shutdown();
337 EXPECT_TRUE(shutdown_result.is_ok());
338}
339
340TEST_F(MetricExportersTest, MetricExporterFactory) {
341 // Test Prometheus factory
342 metric_export_config prometheus_config;
343 prometheus_config.endpoint = "http://prometheus:9090";
344 prometheus_config.format = metric_export_format::prometheus_text;
345
346 auto prometheus_exporter = metric_exporter_factory::create_exporter(prometheus_config, otel_resource_);
347 EXPECT_TRUE(prometheus_exporter);
348
349 // Test StatsD factory
350 metric_export_config statsd_config;
351 statsd_config.endpoint = "statsd.example.com";
352 statsd_config.port = 8125;
353 statsd_config.format = metric_export_format::statsd_datadog;
354
355 auto statsd_exporter = metric_exporter_factory::create_exporter(statsd_config, otel_resource_);
356 EXPECT_TRUE(statsd_exporter);
357
358 // Test OTLP factory
359 metric_export_config otlp_config;
360 otlp_config.endpoint = "http://otlp-collector:4317";
361 otlp_config.format = metric_export_format::otlp_http_json;
362
363 auto otlp_exporter = metric_exporter_factory::create_exporter(otlp_config, otel_resource_);
364 EXPECT_TRUE(otlp_exporter);
365
366 // Test invalid format
367 metric_export_config invalid_config;
368 invalid_config.endpoint = "http://test";
369 invalid_config.format = static_cast<metric_export_format>(999);
370
371 auto invalid_exporter = metric_exporter_factory::create_exporter(invalid_config, otel_resource_);
372 EXPECT_FALSE(invalid_exporter);
373}
374
375TEST_F(MetricExportersTest, SupportedFormatsQuery) {
376 auto prometheus_formats = metric_exporter_factory::get_supported_formats("prometheus");
377 EXPECT_EQ(prometheus_formats.size(), 2);
378 EXPECT_TRUE(std::find(prometheus_formats.begin(), prometheus_formats.end(),
379 metric_export_format::prometheus_text) != prometheus_formats.end());
380 EXPECT_TRUE(std::find(prometheus_formats.begin(), prometheus_formats.end(),
381 metric_export_format::prometheus_protobuf) != prometheus_formats.end());
382
383 auto statsd_formats = metric_exporter_factory::get_supported_formats("statsd");
384 EXPECT_EQ(statsd_formats.size(), 2);
385 EXPECT_TRUE(std::find(statsd_formats.begin(), statsd_formats.end(),
386 metric_export_format::statsd_plain) != statsd_formats.end());
387 EXPECT_TRUE(std::find(statsd_formats.begin(), statsd_formats.end(),
388 metric_export_format::statsd_datadog) != statsd_formats.end());
389
390 auto otlp_formats = metric_exporter_factory::get_supported_formats("otlp");
391 EXPECT_EQ(otlp_formats.size(), 3);
392 EXPECT_TRUE(std::find(otlp_formats.begin(), otlp_formats.end(),
393 metric_export_format::otlp_grpc) != otlp_formats.end());
394
395 auto unknown_formats = metric_exporter_factory::get_supported_formats("unknown");
396 EXPECT_EQ(unknown_formats.size(), 0);
397}
398
399TEST_F(MetricExportersTest, HelperFunctions) {
400 // Test Prometheus helper
401 auto prometheus_exporter = create_prometheus_exporter(9090, "test_job");
402 EXPECT_TRUE(prometheus_exporter);
403
404 // Test StatsD helper
405 auto statsd_exporter = create_statsd_exporter("localhost", 8125, true);
406 EXPECT_TRUE(statsd_exporter);
407
408 // Test OTLP helper
409 auto otlp_exporter = create_otlp_metrics_exporter("http://otlp:4317", otel_resource_,
410 metric_export_format::otlp_http_json);
411 EXPECT_TRUE(otlp_exporter);
412}
413
414TEST_F(MetricExportersTest, EmptyMetricsHandling) {
415 std::vector<monitoring_data> empty_data;
416 metrics_snapshot empty_snapshot;
417
419 config.endpoint = "http://test:1234";
420 config.format = metric_export_format::prometheus_text;
421
422 prometheus_exporter exporter(config);
423
424 auto data_result = exporter.export_metrics(empty_data);
425 EXPECT_TRUE(data_result.is_ok());
426
427 auto snapshot_result = exporter.export_snapshot(empty_snapshot);
428 EXPECT_TRUE(snapshot_result.is_ok());
429
430 auto stats = exporter.get_stats();
431 EXPECT_EQ(stats["exported_metrics"], 1); // Empty snapshot counts as 1
432 EXPECT_EQ(stats["failed_exports"], 0);
433}
434
435TEST_F(MetricExportersTest, LargeMetricBatch) {
436 // Create a large batch of monitoring data
437 std::vector<monitoring_data> large_batch;
438 for (int i = 0; i < 100; ++i) {
439 monitoring_data data("service_" + std::to_string(i));
440 data.add_metric("requests_total", i * 10.0);
441 data.add_metric("response_time", i * 0.1);
442 data.add_tag("instance", std::to_string(i));
443 large_batch.push_back(data);
444 }
445
447 config.endpoint = "http://test:1234";
448 config.format = metric_export_format::statsd_plain;
449 config.max_batch_size = 50; // Smaller than batch size
450
451 statsd_exporter exporter(config);
452 auto result = exporter.export_metrics(large_batch);
453 EXPECT_TRUE(result.is_ok());
454
455 auto stats = exporter.get_stats();
456 EXPECT_EQ(stats["exported_metrics"], 100);
457}
458
459TEST_F(MetricExportersTest, MetricNameSanitization) {
461 config.endpoint = "http://prometheus:9090";
462 config.format = metric_export_format::prometheus_text;
463
464 prometheus_exporter exporter(config);
465
466 // Create data with problematic metric names
467 monitoring_data data("test_component");
468 data.add_metric("http.requests-total", 100.0); // Contains dots and dashes
469 data.add_metric("123_invalid_start", 50.0); // Starts with number
470 data.add_metric("special@chars#metric", 75.0); // Contains special characters
471
472 auto prom_metrics = exporter.convert_monitoring_data(data);
473 EXPECT_EQ(prom_metrics.size(), 3);
474
475 // Check sanitized names exist
476 std::vector<std::string> expected_names = {"http_requests_total", "_123_invalid_start", "special_chars_metric"};
477 std::vector<std::string> actual_names;
478 for (const auto& metric : prom_metrics) {
479 actual_names.push_back(metric.name);
480 }
481
482 for (const auto& expected_name : expected_names) {
483 EXPECT_NE(std::find(actual_names.begin(), actual_names.end(), expected_name), actual_names.end())
484 << "Expected metric name '" << expected_name << "' not found";
485 }
486}
487
488TEST_F(MetricExportersTest, MetricTypeInference) {
490 config.endpoint = "statsd.example.com";
491 config.port = 8125;
492 config.format = metric_export_format::statsd_plain;
493
494 statsd_exporter exporter(config);
495
496 monitoring_data data("test_service");
497 data.add_metric("requests_count", 100.0); // Should be counter
498 data.add_metric("requests_total", 200.0); // Should be counter
499 data.add_metric("response_time_ms", 250.0); // Should be timer
500 data.add_metric("request_duration", 0.5); // Should be timer
501 data.add_metric("cpu_usage", 75.5); // Should be gauge
502 data.add_metric("memory_available", 1024.0); // Should be gauge
503
504 auto statsd_metrics = exporter.convert_monitoring_data(data);
505
506 // Check inferred types by finding specific metrics
507 auto find_metric = [&](const std::string& name) -> const statsd_metric_data* {
508 auto it = std::find_if(statsd_metrics.begin(), statsd_metrics.end(),
509 [&name](const statsd_metric_data& m) { return m.name == name; });
510 return (it != statsd_metrics.end()) ? &(*it) : nullptr;
511 };
512
513 EXPECT_EQ(find_metric("requests_count")->type, metric_type::counter);
514 EXPECT_EQ(find_metric("requests_total")->type, metric_type::counter);
515 EXPECT_EQ(find_metric("response_time_ms")->type, metric_type::timer);
516 EXPECT_EQ(find_metric("request_duration")->type, metric_type::timer);
517 EXPECT_EQ(find_metric("cpu_usage")->type, metric_type::gauge);
518 EXPECT_EQ(find_metric("memory_available")->type, metric_type::gauge);
519}
520
521// ============================================================================
522// UDP Transport Tests
523// ============================================================================
524
525TEST(UdpTransportTest, StubTransportBasicFunctionality) {
526 auto transport = create_stub_udp_transport();
527 ASSERT_TRUE(transport);
528 EXPECT_TRUE(transport->is_available());
529 EXPECT_EQ(transport->name(), "stub");
530 EXPECT_FALSE(transport->is_connected());
531
532 // Test connection
533 auto connect_result = transport->connect("localhost", 8125);
534 EXPECT_TRUE(connect_result.is_ok());
535 EXPECT_TRUE(transport->is_connected());
536 EXPECT_EQ(transport->get_host(), "localhost");
537 EXPECT_EQ(transport->get_port(), 8125);
538
539 // Test send
540 std::string metric = "test_metric:100|c";
541 auto send_result = transport->send(metric);
542 EXPECT_TRUE(send_result.is_ok());
543
544 // Check statistics
545 auto stats = transport->get_statistics();
546 EXPECT_EQ(stats.packets_sent, 1);
547 EXPECT_EQ(stats.bytes_sent, metric.size());
548 EXPECT_EQ(stats.send_failures, 0);
549
550 // Test disconnect
551 transport->disconnect();
552 EXPECT_FALSE(transport->is_connected());
553
554 // Send should fail after disconnect
555 auto fail_result = transport->send(metric);
556 EXPECT_TRUE(fail_result.is_err());
557
558 auto stats_after = transport->get_statistics();
559 EXPECT_EQ(stats_after.send_failures, 1);
560}
561
562TEST(UdpTransportTest, StubTransportSimulateFailure) {
563 auto transport = create_stub_udp_transport();
564 transport->set_simulate_success(false);
565
566 // Connection should fail
567 auto connect_result = transport->connect("localhost", 8125);
568 EXPECT_TRUE(connect_result.is_err());
569 EXPECT_FALSE(transport->is_connected());
570
571 // Re-enable success and connect
572 transport->set_simulate_success(true);
573 auto retry_result = transport->connect("localhost", 8125);
574 EXPECT_TRUE(retry_result.is_ok());
575
576 // Now simulate send failure
577 transport->set_simulate_success(false);
578 auto send_result = transport->send("test:1|c");
579 EXPECT_TRUE(send_result.is_err());
580}
581
582TEST(UdpTransportTest, StubTransportStatisticsReset) {
583 auto transport = create_stub_udp_transport();
584 transport->connect("localhost", 8125);
585 transport->send("metric1:1|c");
586 transport->send("metric2:2|c");
587 transport->send("metric3:3|c");
588
589 auto stats = transport->get_statistics();
590 EXPECT_EQ(stats.packets_sent, 3);
591 EXPECT_GT(stats.bytes_sent, 0);
592
593 transport->reset_statistics();
594 auto reset_stats = transport->get_statistics();
595 EXPECT_EQ(reset_stats.packets_sent, 0);
596 EXPECT_EQ(reset_stats.bytes_sent, 0);
597 EXPECT_EQ(reset_stats.send_failures, 0);
598}
599
600TEST(UdpTransportTest, DefaultTransportCreation) {
601 auto transport = create_default_udp_transport();
602 ASSERT_TRUE(transport);
603 EXPECT_TRUE(transport->is_available());
604 // Default transport should work (either stub or network)
605}
606
607// ============================================================================
608// gRPC Transport Tests
609// ============================================================================
610
611TEST(GrpcTransportTest, StubTransportBasicFunctionality) {
612 auto transport = create_stub_grpc_transport();
613 ASSERT_TRUE(transport);
614 EXPECT_TRUE(transport->is_available());
615 EXPECT_EQ(transport->name(), "stub");
616 EXPECT_FALSE(transport->is_connected());
617
618 // Test connection
619 auto connect_result = transport->connect("localhost", 4317);
620 EXPECT_TRUE(connect_result.is_ok());
621 EXPECT_TRUE(transport->is_connected());
622 EXPECT_EQ(transport->get_host(), "localhost");
623 EXPECT_EQ(transport->get_port(), 4317);
624
625 // Test send
626 grpc_request request;
627 request.service = "test.Service";
628 request.method = "TestMethod";
629 request.body = {0x01, 0x02, 0x03, 0x04};
630 request.timeout = std::chrono::milliseconds(5000);
631
632 auto send_result = transport->send(request);
633 EXPECT_TRUE(send_result.is_ok());
634
635 const auto& response = send_result.value();
636 EXPECT_EQ(response.status_code, 0); // gRPC OK
637 EXPECT_EQ(response.status_message, "OK");
638
639 // Check statistics
640 auto stats = transport->get_statistics();
641 EXPECT_EQ(stats.requests_sent, 1);
642 EXPECT_EQ(stats.bytes_sent, 4);
643 EXPECT_EQ(stats.send_failures, 0);
644
645 // Test disconnect
646 transport->disconnect();
647 EXPECT_FALSE(transport->is_connected());
648}
649
650TEST(GrpcTransportTest, StubTransportCustomResponseHandler) {
651 auto transport = create_stub_grpc_transport();
652 transport->connect("localhost", 4317);
653
654 // Set custom response handler
655 transport->set_response_handler([](const grpc_request& req) {
656 grpc_response response;
657 response.status_code = 0;
658 response.status_message = "Custom response for " + req.method;
659 response.body = {0xAB, 0xCD};
660 return response;
661 });
662
663 grpc_request request;
664 request.method = "CustomMethod";
665 request.body = {0x01};
666
667 auto result = transport->send(request);
668 EXPECT_TRUE(result.is_ok());
669 EXPECT_EQ(result.value().status_message, "Custom response for CustomMethod");
670 EXPECT_EQ(result.value().body.size(), 2);
671}
672
673TEST(GrpcTransportTest, StubTransportSimulateFailure) {
674 auto transport = create_stub_grpc_transport();
675 transport->set_simulate_success(false);
676
677 // Connection should fail
678 auto connect_result = transport->connect("localhost", 4317);
679 EXPECT_TRUE(connect_result.is_err());
680
681 // Re-enable and connect
682 transport->set_simulate_success(true);
683 transport->connect("localhost", 4317);
684
685 // Simulate send failure
686 transport->set_simulate_success(false);
687 grpc_request request;
688 request.body = {0x01};
689 auto send_result = transport->send(request);
690 EXPECT_TRUE(send_result.is_err());
691
692 auto stats = transport->get_statistics();
693 EXPECT_EQ(stats.send_failures, 1);
694}
695
696TEST(GrpcTransportTest, DefaultTransportCreation) {
697 auto transport = create_default_grpc_transport();
698 ASSERT_TRUE(transport);
699 EXPECT_TRUE(transport->is_available());
700}
701
702// ============================================================================
703// StatsD Exporter with Custom Transport Tests
704// ============================================================================
705
706TEST_F(MetricExportersTest, StatsDExporterWithCustomTransport) {
707 auto stub_transport = create_stub_udp_transport();
708 auto* transport_ptr = stub_transport.get();
709
711 config.endpoint = "statsd.example.com";
712 config.port = 8125;
713 config.format = metric_export_format::statsd_datadog;
714
715 statsd_exporter exporter(config, std::move(stub_transport));
716
717 // Start the exporter
718 auto start_result = exporter.start();
719 EXPECT_TRUE(start_result.is_ok());
720
721 // Export metrics
722 std::vector<monitoring_data> data_batch = {test_data_};
723 auto export_result = exporter.export_metrics(data_batch);
724 EXPECT_TRUE(export_result.is_ok());
725
726 // Check transport was used
727 auto transport_stats = transport_ptr->get_statistics();
728 EXPECT_GT(transport_stats.packets_sent, 0);
729 EXPECT_GT(transport_stats.bytes_sent, 0);
730
731 // Get exporter stats (includes transport stats)
732 auto stats = exporter.get_stats();
733 EXPECT_GT(stats["transport_packets_sent"], 0);
734 EXPECT_GT(stats["transport_bytes_sent"], 0);
735
736 // Stop the exporter
737 auto stop_result = exporter.stop();
738 EXPECT_TRUE(stop_result.is_ok());
739}
740
741TEST_F(MetricExportersTest, StatsDExporterTransportFailure) {
742 auto stub_transport = create_stub_udp_transport();
743 stub_transport->set_simulate_success(false);
744
746 config.endpoint = "statsd.example.com";
747 config.port = 8125;
748 config.format = metric_export_format::statsd_plain;
749
750 statsd_exporter exporter(config, std::move(stub_transport));
751
752 // Export should fail due to transport failure
753 std::vector<monitoring_data> data_batch = {test_data_};
754 auto export_result = exporter.export_metrics(data_batch);
755 EXPECT_TRUE(export_result.is_err());
756
757 auto stats = exporter.get_stats();
758 EXPECT_EQ(stats["failed_exports"], 1);
759}
760
761// ============================================================================
762// OTLP Exporter with Custom Transport Tests
763// ============================================================================
764
765TEST_F(MetricExportersTest, OtlpExporterWithCustomHttpTransport) {
766 auto stub_http = create_stub_transport();
767 auto stub_grpc = create_stub_grpc_transport();
768
770 config.endpoint = "http://otlp-collector";
771 config.port = 4318;
772 config.format = metric_export_format::otlp_http_json;
773
774 otlp_metrics_exporter exporter(config, otel_resource_,
775 std::move(stub_http), std::move(stub_grpc));
776
777 // Start and export
778 auto start_result = exporter.start();
779 EXPECT_TRUE(start_result.is_ok());
780
781 std::vector<monitoring_data> data_batch = {test_data_};
782 auto export_result = exporter.export_metrics(data_batch);
783 EXPECT_TRUE(export_result.is_ok());
784
785 auto stats = exporter.get_stats();
786 EXPECT_EQ(stats["exported_metrics"], 1);
787
788 auto stop_result = exporter.stop();
789 EXPECT_TRUE(stop_result.is_ok());
790}
791
792// ============================================================================
793// simple_http_client URL Parser Tests
794// ============================================================================
795
796TEST(HttpTransportTest, SimpleHttpClientValidUrls) {
798
799 // Stub transport returns error for all valid URLs (no real HTTP client)
800 http_request req;
801 req.url = "http://example.com/api/v1/traces";
802 auto result = client.send(req);
803 EXPECT_TRUE(result.is_err());
804
805 req.url = "https://collector.example.com:4318/v1/metrics";
806 result = client.send(req);
807 EXPECT_TRUE(result.is_err());
808
809 req.url = "http://localhost:9090";
810 result = client.send(req);
811 EXPECT_TRUE(result.is_err());
812
813 req.url = "http://prometheus";
814 result = client.send(req);
815 EXPECT_TRUE(result.is_err());
816}
817
818TEST(HttpTransportTest, SimpleHttpClientInvalidUrls) {
820
821 // Missing scheme
822 http_request req;
823 req.url = "example.com/api";
824 auto result = client.send(req);
825 EXPECT_TRUE(result.is_err());
826
827 // Empty host after scheme
828 req.url = "http://";
829 result = client.send(req);
830 EXPECT_TRUE(result.is_err());
831
832 // No URL at all
833 req.url = "";
834 result = client.send(req);
835 EXPECT_TRUE(result.is_err());
836}
837
838TEST(HttpTransportTest, SimpleHttpClientPortDefaults) {
840
841 // Stub transport returns error (no real HTTP client) — URL parsing still works
842 http_request req;
843 req.url = "http://example.com/path";
844 auto result = client.send(req);
845 EXPECT_TRUE(result.is_err());
846
847 req.url = "https://example.com/path";
848 result = client.send(req);
849 EXPECT_TRUE(result.is_err());
850}
851
852TEST(HttpTransportTest, SimpleHttpClientNameAndAvailability) {
854 EXPECT_EQ(client.name(), "stub");
855 EXPECT_FALSE(client.is_available());
856}
857
858// ============================================================================
859// stub_http_transport Explicit Tests
860// ============================================================================
861
862TEST(HttpTransportTest, StubHttpTransportIsAvailableAndName) {
863 auto transport = create_stub_transport();
864 ASSERT_TRUE(transport);
865 EXPECT_TRUE(transport->is_available());
866 EXPECT_EQ(transport->name(), "stub");
867}
868
869// ============================================================================
870// stub_grpc_transport Additional Tests
871// ============================================================================
872
873TEST(GrpcTransportTest, StubTransportResetStatistics) {
874 auto transport = create_stub_grpc_transport();
875 transport->connect("localhost", 4317);
876
877 grpc_request request;
878 request.body = {0x01, 0x02, 0x03};
879 transport->send(request);
880 transport->send(request);
881
882 auto stats = transport->get_statistics();
883 EXPECT_EQ(stats.requests_sent, 2);
884 EXPECT_EQ(stats.bytes_sent, 6);
885
886 transport->reset_statistics();
887 auto reset_stats = transport->get_statistics();
888 EXPECT_EQ(reset_stats.requests_sent, 0);
889 EXPECT_EQ(reset_stats.bytes_sent, 0);
890 EXPECT_EQ(reset_stats.send_failures, 0);
891}
892
893TEST(GrpcTransportTest, StubTransportDisconnectedState) {
894 auto transport = create_stub_grpc_transport();
895 transport->connect("localhost", 4317);
896 EXPECT_TRUE(transport->is_connected());
897
898 transport->disconnect();
899 EXPECT_FALSE(transport->is_connected());
900
901 // Send should fail after disconnect
902 grpc_request request;
903 request.body = {0x01};
904 auto result = transport->send(request);
905 EXPECT_TRUE(result.is_err());
906
907 auto stats = transport->get_statistics();
908 EXPECT_EQ(stats.send_failures, 1);
909}
910
911// ============================================================================
912// stub_udp_transport String Overload Test
913// ============================================================================
914
915TEST(UdpTransportTest, StubTransportStringSendDelegation) {
916 auto transport = create_stub_udp_transport();
917 transport->connect("localhost", 8125);
918
919 // Send using the string overload (inherited from udp_transport base class)
920 std::string metric_line = "test.metric:42|g";
921 auto result = transport->send(metric_line);
922 EXPECT_TRUE(result.is_ok());
923
924 auto stats = transport->get_statistics();
925 EXPECT_EQ(stats.packets_sent, 1);
926 EXPECT_EQ(stats.bytes_sent, metric_line.size());
927}
928
929// ============================================================================
930// OTLP Exporter with Custom Transport Tests (continued)
931// ============================================================================
932
933TEST_F(MetricExportersTest, OtlpExporterWithCustomGrpcTransport) {
934 auto stub_http = create_stub_transport();
935 auto stub_grpc = create_stub_grpc_transport();
936 auto* grpc_ptr = stub_grpc.get();
937
939 config.endpoint = "otlp-collector";
940 config.port = 4317;
941 config.format = metric_export_format::otlp_grpc;
942
943 otlp_metrics_exporter exporter(config, otel_resource_,
944 std::move(stub_http), std::move(stub_grpc));
945
946 std::vector<monitoring_data> data_batch = {test_data_};
947 auto export_result = exporter.export_metrics(data_batch);
948 EXPECT_TRUE(export_result.is_ok());
949
950 // Check gRPC transport was used
951 auto transport_stats = grpc_ptr->get_statistics();
952 EXPECT_GT(transport_stats.requests_sent, 0);
953
954 auto stats = exporter.get_stats();
955 EXPECT_GT(stats["transport_requests_sent"], 0);
956}
metrics_snapshot create_test_snapshot()
metrics_snapshot test_snapshot_
monitoring_data create_test_monitoring_data()
static std::vector< metric_export_format > get_supported_formats(const std::string &backend)
Get supported formats for a specific backend.
static std::unique_ptr< metric_exporter_interface > create_exporter(const metric_export_config &config, const otel_resource &resource=create_service_resource("monitoring_system", "2.0.0"))
Create a metric exporter based on format.
OpenTelemetry Protocol (OTLP) trace exporter implementation.
OpenTelemetry Protocol (OTLP) metrics exporter implementation.
std::unordered_map< std::string, std::size_t > get_stats() const override
Get exporter statistics.
common::VoidResult export_metrics(const std::vector< monitoring_data > &metrics) override
Export a batch of metrics.
common::VoidResult start() override
Start the exporter (for pull-based systems)
common::VoidResult export_snapshot(const metrics_snapshot &snapshot) override
Export a single metrics snapshot.
common::VoidResult shutdown() override
Shutdown the exporter.
common::VoidResult stop() override
Stop the exporter.
common::VoidResult flush() override
Flush any pending metrics.
Prometheus metric exporter implementation.
common::VoidResult flush() override
Flush any pending metrics.
std::string get_metrics_text() const
Get current metrics in Prometheus format (for HTTP endpoint)
std::unordered_map< std::string, std::size_t > get_stats() const override
Get exporter statistics.
std::vector< prometheus_metric_data > convert_snapshot(const metrics_snapshot &snapshot) const
Convert metrics_snapshot to Prometheus format.
common::VoidResult shutdown() override
Shutdown the exporter.
common::VoidResult export_snapshot(const metrics_snapshot &snapshot) override
Export a single metrics snapshot.
std::vector< prometheus_metric_data > convert_monitoring_data(const monitoring_data &data) const
Convert monitoring_data to Prometheus format.
common::VoidResult export_metrics(const std::vector< monitoring_data > &metrics) override
Export a batch of metrics.
Simple HTTP client using basic socket operations.
StatsD metric exporter implementation.
std::unordered_map< std::string, std::size_t > get_stats() const override
Get exporter statistics.
common::VoidResult shutdown() override
Shutdown the exporter.
common::VoidResult stop() override
Stop the exporter.
std::vector< statsd_metric_data > convert_snapshot(const metrics_snapshot &snapshot) const
Convert metrics_snapshot to StatsD format.
common::VoidResult start() override
Start the exporter (for pull-based systems)
common::VoidResult export_snapshot(const metrics_snapshot &snapshot) override
Export a single metrics snapshot.
common::VoidResult flush() override
Flush any pending metrics.
common::VoidResult export_metrics(const std::vector< monitoring_data > &metrics) override
Export a batch of metrics.
std::vector< statsd_metric_data > convert_monitoring_data(const monitoring_data &data) const
Convert monitoring_data to StatsD format.
gRPC transport layer for OTLP exporters
HTTP transport layer for trace exporters.
Metric data exporters for various monitoring and observability systems.
Interface for components that expose monitoring metrics.
Core monitoring system interface definitions.
metric_export_format
Supported metric export formats.
@ prometheus_text
Prometheus text exposition format.
std::unique_ptr< grpc_transport > create_default_grpc_transport()
Create default gRPC transport.
std::unique_ptr< stub_grpc_transport > create_stub_grpc_transport()
Create stub gRPC transport for testing.
std::unique_ptr< prometheus_exporter > create_prometheus_exporter(std::uint16_t port=9090, const std::string &job_name="monitoring_system")
Helper function to create a Prometheus exporter.
std::unique_ptr< stub_udp_transport > create_stub_udp_transport()
Create stub UDP transport for testing.
std::unique_ptr< udp_transport > create_default_udp_transport()
Create default UDP transport.
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_metrics_exporter > create_otlp_metrics_exporter(const std::string &endpoint, const otel_resource &resource, metric_export_format format=metric_export_format::otlp_grpc)
Helper function to create an OTLP metrics exporter.
std::unique_ptr< stub_http_transport > create_stub_transport()
Create stub HTTP transport for testing.
std::unique_ptr< statsd_exporter > create_statsd_exporter(const std::string &host="localhost", std::uint16_t port=8125, bool datadog_format=false)
Helper function to create a StatsD exporter.
OpenTelemetry compatibility layer for monitoring system integration.
gRPC request configuration
std::chrono::milliseconds timeout
std::vector< uint8_t > body
HTTP request configuration.
Configuration for metric exporters.
std::unordered_map< std::string, std::string > labels
Default labels/tags.
std::size_t max_batch_size
Maximum metrics per batch.
std::chrono::milliseconds push_interval
Push interval for push-based systems.
std::string instance_id
Instance identifier.
std::uint16_t port
Port number (for UDP/TCP)
std::size_t max_queue_size
Maximum queued metrics.
std::string endpoint
Endpoint URL or address.
common::VoidResult validate() const
Validate export configuration.
Basic metric structure for interface compatibility.
std::chrono::system_clock::time_point timestamp
std::variant< double, int64_t, std::string > value
Complete snapshot of metrics at a point in time.
std::chrono::system_clock::time_point capture_time
std::vector< metric_value > metrics
void add_metric(const std::string &name, double value)
Add a metric to the snapshot.
Container for monitoring metrics from a component.
void add_metric(const std::string &key, double value)
Add a numeric metric.
void add_tag(const std::string &key, const std::string &value)
Add a tag (string metadata)
OpenTelemetry resource representation.
Prometheus-specific metric representation.
StatsD-specific metric representation.
std::unordered_map< std::string, std::string > tags
std::string to_statsd_format(bool datadog_format=false) const
Convert to StatsD format.
TEST(UdpTransportTest, StubTransportBasicFunctionality)
TEST_F(MetricExportersTest, MetricExportConfigValidation)
UDP transport layer for metric exporters.