Skip to main content

libdd_telemetry/data/
payload.rs

1// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4use 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        // With SCA metadata: per the telemetry schema, `type` names the metadata kind and
552        // `value` is an opaque stringified-JSON payload that consumers must parse.
553        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        // SCA enabled with no findings: `metadata` is present-but-empty (distinct from omitted).
581        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}