1use crate::data::*;
5use serde::Serialize;
6
7#[derive(Serialize, Debug)]
8#[serde(tag = "request_type", content = "payload")]
9#[serde(rename_all = "kebab-case")]
10pub enum Payload {
11 AppStarted(AppStarted),
12 AppDependenciesLoaded(AppDependenciesLoaded),
13 AppIntegrationsChange(AppIntegrationsChange),
14 AppProductChange(AppProductChange),
15 AppClientConfigurationChange(AppClientConfigurationChange),
16 AppEndpoints(AppEndpoints),
17 AppHeartbeat(#[serde(skip_serializing)] ()),
18 AppClosing(#[serde(skip_serializing)] ()),
19 GenerateMetrics(GenerateMetrics),
20 Sketches(Distributions),
21 Logs(Logs),
22 MessageBatch(Vec<Payload>),
23 AppExtendedHeartbeat(AppStarted),
24}
25
26impl Payload {
27 pub fn request_type(&self) -> &'static str {
28 use Payload::*;
29 match self {
30 AppStarted(_) => "app-started",
31 AppDependenciesLoaded(_) => "app-dependencies-loaded",
32 AppIntegrationsChange(_) => "app-integrations-change",
33 AppProductChange(_) => "app-product-change",
34 AppClientConfigurationChange(_) => "app-client-configuration-change",
35 AppEndpoints(_) => "app-endpoints",
36 AppHeartbeat(_) => "app-heartbeat",
37 AppClosing(_) => "app-closing",
38 GenerateMetrics(_) => "generate-metrics",
39 Sketches(_) => "sketches",
40 Logs(_) => "logs",
41 MessageBatch(_) => "message-batch",
42 AppExtendedHeartbeat(_) => "app-extended-heartbeat",
43 }
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50 use serde_json::json;
51
52 #[test]
53 fn test_app_started_serialization() {
54 let payload = Payload::AppStarted(AppStarted {
55 configuration: vec![
56 Configuration {
57 name: "sampling_rate".to_string(),
58 value: Some("0.5".to_string()),
59 origin: ConfigurationOrigin::EnvVar,
60 config_id: Some("config-123".to_string()),
61 seq_id: Some(42),
62 },
63 Configuration {
64 name: "log_level".to_string(),
65 value: Some("debug".to_string()),
66 origin: ConfigurationOrigin::Code,
67 config_id: None,
68 seq_id: None,
69 },
70 ],
71 dependencies: Vec::new(),
72 integrations: Vec::new(),
73 install_signature: None,
74 products: Default::default(),
75 error: None,
76 });
77
78 let serialized = serde_json::to_value(&payload).unwrap();
79
80 let expected = json!({
81 "request_type": "app-started",
82 "payload": {
83 "configuration": [
84 {
85 "name": "sampling_rate",
86 "value": "0.5",
87 "origin": "env_var",
88 "config_id": "config-123",
89 "seq_id": 42
90 },
91 {
92 "name": "log_level",
93 "value": "debug",
94 "origin": "code",
95 "config_id": null,
96 "seq_id": null
97 }
98 ],
99 "dependencies": [],
100 "integrations": []
101 }
102 });
103
104 assert_eq!(serialized, expected);
105 }
106
107 #[test]
108 fn test_app_dependencies_loaded_serialization() {
109 let payload = Payload::AppDependenciesLoaded(AppDependenciesLoaded {
110 dependencies: vec![
111 Dependency {
112 name: "tokio".to_string(),
113 version: Some("1.32.0".to_string()),
114 ..Default::default()
115 },
116 Dependency {
117 name: "serde".to_string(),
118 ..Default::default()
119 },
120 ],
121 });
122
123 let serialized = serde_json::to_value(&payload).unwrap();
124
125 let expected = json!({
126 "request_type": "app-dependencies-loaded",
127 "payload": {
128 "dependencies": [
129 {
130 "name": "tokio",
131 "version": "1.32.0",
132 "hash": null,
133 "metadata": null
134 },
135 {
136 "name": "serde",
137 "version": null,
138 "hash": null,
139 "metadata": null
140 }
141 ]
142 }
143 });
144
145 assert_eq!(serialized, expected);
146 }
147
148 #[test]
149 fn test_app_integrations_change_serialization() {
150 let payload = Payload::AppIntegrationsChange(AppIntegrationsChange {
151 integrations: vec![
152 Integration {
153 name: "postgres".to_string(),
154 enabled: true,
155 version: Some("0.19.0".to_string()),
156 compatible: Some(true),
157 auto_enabled: Some(false),
158 ..Default::default()
159 },
160 Integration {
161 name: "redis".to_string(),
162 enabled: false,
163 version: None,
164 compatible: None,
165 auto_enabled: None,
166 error: Some("patch failed: boom".to_string()),
167 },
168 ],
169 });
170
171 let serialized = serde_json::to_value(&payload).unwrap();
172
173 let expected = json!({
174 "request_type": "app-integrations-change",
175 "payload": {
176 "integrations": [
177 {
178 "name": "postgres",
179 "enabled": true,
180 "version": "0.19.0",
181 "compatible": true,
182 "auto_enabled": false,
183 "error": null
184 },
185 {
186 "name": "redis",
187 "enabled": false,
188 "version": null,
189 "compatible": null,
190 "auto_enabled": null,
191 "error": "patch failed: boom"
192 }
193 ]
194 }
195 });
196
197 assert_eq!(serialized, expected);
198 }
199
200 #[test]
201 fn test_app_client_configuration_change_serialization() {
202 let payload = Payload::AppClientConfigurationChange(AppClientConfigurationChange {
203 configuration: vec![Configuration {
204 name: "timeout".to_string(),
205 value: Some("30s".to_string()),
206 origin: ConfigurationOrigin::RemoteConfig,
207 config_id: Some("remote-1".to_string()),
208 seq_id: Some(10),
209 }],
210 });
211
212 let serialized = serde_json::to_value(&payload).unwrap();
213
214 let expected = json!({
215 "request_type": "app-client-configuration-change",
216 "payload": {
217 "configuration": [
218 {
219 "name": "timeout",
220 "value": "30s",
221 "origin": "remote_config",
222 "config_id": "remote-1",
223 "seq_id": 10
224 }
225 ]
226 }
227 });
228
229 assert_eq!(serialized, expected);
230 }
231
232 #[test]
233 fn test_app_endpoints_serialization() {
234 let payload = Payload::AppEndpoints(AppEndpoints {
235 is_first: true,
236 endpoints: vec![
237 json!({
238 "method": "GET",
239 "path": "/api/users",
240 "operation_name": "get_users",
241 "resource_name": "users"
242 }),
243 json!({
244 "method": "POST",
245 "path": "/api/users",
246 "operation_name": "create_user",
247 "resource_name": "users"
248 }),
249 ],
250 });
251
252 let serialized = serde_json::to_value(&payload).unwrap();
253
254 let expected = json!({
255 "request_type": "app-endpoints",
256 "payload": {
257 "is_first": true,
258 "endpoints": [
259 {
260 "method": "GET",
261 "path": "/api/users",
262 "operation_name": "get_users",
263 "resource_name": "users"
264 },
265 {
266 "method": "POST",
267 "path": "/api/users",
268 "operation_name": "create_user",
269 "resource_name": "users"
270 }
271 ]
272 }
273 });
274
275 assert_eq!(serialized, expected);
276 }
277
278 #[test]
279 fn test_app_heartbeat_serialization() {
280 let payload = Payload::AppHeartbeat(());
281
282 let serialized = serde_json::to_value(&payload).unwrap();
283
284 let expected = json!({
285 "request_type": "app-heartbeat"
286 });
287
288 assert_eq!(serialized, expected);
289 }
290
291 #[test]
292 fn test_app_closing_serialization() {
293 let payload = Payload::AppClosing(());
294
295 let serialized = serde_json::to_value(&payload).unwrap();
296
297 let expected = json!({
298 "request_type": "app-closing"
299 });
300
301 assert_eq!(serialized, expected);
302 }
303
304 #[test]
305 fn test_generate_metrics_serialization() {
306 let payload = Payload::GenerateMetrics(GenerateMetrics {
307 series: vec![
308 metrics::Serie {
309 namespace: metrics::MetricNamespace::Tracers,
310 metric: "spans_created".to_string(),
311 points: vec![(1234567890, 42.0), (1234567900, 43.0)],
312 tags: vec![],
313 common: true,
314 _type: metrics::MetricType::Count,
315 interval: 10,
316 },
317 metrics::Serie {
318 namespace: metrics::MetricNamespace::Profilers,
319 metric: "cpu_time".to_string(),
320 points: vec![(1234567890, 0.75)],
321 tags: vec![],
322 common: false,
323 _type: metrics::MetricType::Gauge,
324 interval: 60,
325 },
326 ],
327 });
328
329 let serialized = serde_json::to_value(&payload).unwrap();
330
331 let expected = json!({
332 "request_type": "generate-metrics",
333 "payload": {
334 "series": [
335 {
336 "namespace": "tracers",
337 "metric": "spans_created",
338 "points": [[1234567890, 42.0], [1234567900, 43.0]],
339 "tags": [],
340 "common": true,
341 "type": "count",
342 "interval": 10
343 },
344 {
345 "namespace": "profilers",
346 "metric": "cpu_time",
347 "points": [[1234567890, 0.75]],
348 "tags": [],
349 "common": false,
350 "type": "gauge",
351 "interval": 60
352 }
353 ]
354 }
355 });
356
357 assert_eq!(serialized, expected);
358 }
359
360 #[test]
361 fn test_sketches_serialization() {
362 let payload = Payload::Sketches(Distributions {
363 series: vec![metrics::Distribution {
364 namespace: metrics::MetricNamespace::Tracers,
365 metric: "request_duration".to_string(),
366 tags: vec![],
367 sketch: metrics::SerializedSketch::B64 {
368 sketch_b64: "base64encodeddata".to_string(),
369 },
370 common: true,
371 interval: 10,
372 _type: metrics::MetricType::Distribution,
373 }],
374 });
375
376 let serialized = serde_json::to_value(&payload).unwrap();
377
378 let expected = json!({
379 "request_type": "sketches",
380 "payload": {
381 "series": [
382 {
383 "namespace": "tracers",
384 "metric": "request_duration",
385 "tags": [],
386 "sketch_b64": "base64encodeddata",
387 "common": true,
388 "interval": 10,
389 "type": "distribution"
390 }
391 ]
392 }
393 });
394
395 assert_eq!(serialized, expected);
396 }
397
398 #[test]
399 fn test_logs_serialization() {
400 let payload = Payload::Logs(Logs {
401 logs: vec![
402 Log {
403 message: "Connection error".to_string(),
404 level: LogLevel::Error,
405 count: 1,
406 stack_trace: Some("at main.rs:42".to_string()),
407 tags: "env:prod".to_string(),
408 is_sensitive: false,
409 is_crash: false,
410 },
411 Log {
412 message: "Deprecated function used".to_string(),
413 level: LogLevel::Warn,
414 count: 5,
415 stack_trace: None,
416 tags: String::new(),
417 is_sensitive: false,
418 is_crash: false,
419 },
420 ],
421 });
422
423 let serialized = serde_json::to_value(&payload).unwrap();
424
425 let expected = json!({
426 "request_type": "logs",
427 "payload": {
428 "logs": [
429 {
430 "message": "Connection error",
431 "level": "ERROR",
432 "count": 1,
433 "stack_trace": "at main.rs:42",
434 "tags": "env:prod",
435 "is_sensitive": false,
436 "is_crash": false
437 },
438 {
439 "message": "Deprecated function used",
440 "level": "WARN",
441 "count": 5,
442 "stack_trace": null,
443 "tags": "",
444 "is_sensitive": false,
445 "is_crash": false
446 }
447 ]
448 }
449 });
450
451 assert_eq!(serialized, expected);
452 }
453
454 #[test]
455 fn test_message_batch_serialization() {
456 let payload = Payload::MessageBatch(vec![
457 Payload::AppHeartbeat(()),
458 Payload::Logs(Logs {
459 logs: vec![Log {
460 message: "Test log".to_string(),
461 level: LogLevel::Debug,
462 count: 1,
463 stack_trace: None,
464 tags: String::new(),
465 is_sensitive: false,
466 is_crash: false,
467 }],
468 }),
469 ]);
470
471 let serialized = serde_json::to_value(&payload).unwrap();
472
473 let expected = json!({
474 "request_type": "message-batch",
475 "payload": [
476 {
477 "request_type": "app-heartbeat"
478 },
479 {
480 "request_type": "logs",
481 "payload": {
482 "logs": [
483 {
484 "message": "Test log",
485 "level": "DEBUG",
486 "count": 1,
487 "stack_trace": null,
488 "tags": "",
489 "is_sensitive": false,
490 "is_crash": false
491 }
492 ]
493 }
494 }
495 ]
496 });
497
498 assert_eq!(serialized, expected);
499 }
500
501 #[test]
502 fn test_app_product_change_serialization() {
503 let mut products = std::collections::HashMap::new();
504 products.insert(
505 "appsec".to_string(),
506 ProductState {
507 enabled: true,
508 version: Some("1.2.3".to_string()),
509 error: None,
510 },
511 );
512 let payload = Payload::AppProductChange(AppProductChange { products });
513
514 let serialized = serde_json::to_value(&payload).unwrap();
515
516 let expected = json!({
517 "request_type": "app-product-change",
518 "payload": {
519 "products": {
520 "appsec": {
521 "enabled": true,
522 "version": "1.2.3",
523 "error": null
524 }
525 }
526 }
527 });
528
529 assert_eq!(serialized, expected);
530 }
531
532 #[test]
533 fn test_dependency_metadata_serialization() {
534 let plain = Payload::AppDependenciesLoaded(AppDependenciesLoaded {
535 dependencies: vec![Dependency {
536 name: "requests".to_string(),
537 version: Some("2.0".to_string()),
538 ..Default::default()
539 }],
540 });
541 assert_eq!(
542 serde_json::to_value(&plain).unwrap(),
543 json!({
544 "request_type": "app-dependencies-loaded",
545 "payload": { "dependencies": [{
546 "name": "requests", "version": "2.0", "hash": null, "metadata": null
547 }] }
548 })
549 );
550
551 let with_sca = Payload::AppDependenciesLoaded(AppDependenciesLoaded {
554 dependencies: vec![Dependency {
555 name: "requests".to_string(),
556 version: Some("2.0".to_string()),
557 metadata: Some(vec![DependencyMetadata {
558 r#type: "reachability".to_string(),
559 value: "{\"id\":\"CVE-2024-1\",\"reached\":true}".to_string(),
560 }]),
561 ..Default::default()
562 }],
563 });
564 assert_eq!(
565 serde_json::to_value(&with_sca).unwrap(),
566 json!({
567 "request_type": "app-dependencies-loaded",
568 "payload": { "dependencies": [{
569 "name": "requests",
570 "version": "2.0",
571 "hash": null,
572 "metadata": [{
573 "type": "reachability",
574 "value": "{\"id\":\"CVE-2024-1\",\"reached\":true}"
575 }]
576 }] }
577 })
578 );
579
580 let empty_meta = Payload::AppDependenciesLoaded(AppDependenciesLoaded {
582 dependencies: vec![Dependency {
583 name: "requests".to_string(),
584 version: Some("2.0".to_string()),
585 metadata: Some(vec![]),
586 ..Default::default()
587 }],
588 });
589 assert_eq!(
590 serde_json::to_value(&empty_meta).unwrap(),
591 json!({
592 "request_type": "app-dependencies-loaded",
593 "payload": { "dependencies": [{
594 "name": "requests", "version": "2.0", "hash": null, "metadata": []
595 }] }
596 })
597 );
598 }
599
600 #[test]
601 fn test_app_extended_heartbeat_serialization() {
602 let payload = Payload::AppExtendedHeartbeat(AppStarted {
603 configuration: vec![Configuration {
604 name: "feature_flag".to_string(),
605 value: Some("enabled".to_string()),
606 origin: ConfigurationOrigin::Default,
607 config_id: None,
608 seq_id: None,
609 }],
610 dependencies: Vec::new(),
611 integrations: Vec::new(),
612 install_signature: None,
613 products: Default::default(),
614 error: None,
615 });
616
617 let serialized = serde_json::to_value(&payload).unwrap();
618
619 let expected = json!({
620 "request_type": "app-extended-heartbeat",
621 "payload": {
622 "configuration": [
623 {
624 "name": "feature_flag",
625 "value": "enabled",
626 "origin": "default",
627 "config_id": null,
628 "seq_id": null
629 }
630 ],
631 "dependencies": [],
632 "integrations": []
633 }
634 });
635
636 assert_eq!(serialized, expected);
637 }
638}