1
- // Copyright (c) 2020 Zededa, Inc.
1
+ // Copyright (c) 2020-2025 Zededa, Inc.
2
2
// SPDX-License-Identifier: Apache-2.0
3
3
4
4
package zedrouter
7
7
"bytes"
8
8
"context"
9
9
"encoding/json"
10
+ "fmt"
10
11
"io"
11
12
"net/http"
12
13
"strconv"
@@ -16,10 +17,14 @@ import (
16
17
apitypes "github.com/docker/docker/api/types"
17
18
"github.com/docker/docker/client"
18
19
"github.com/docker/docker/pkg/stdcopy"
20
+ "github.com/google/go-cmp/cmp"
21
+ nestedapp "github.com/lf-edge/eve-tools/runtimemetrics/go/nestedappinstancemetrics"
19
22
"github.com/lf-edge/eve/pkg/pillar/agentlog"
20
23
"github.com/lf-edge/eve/pkg/pillar/types"
21
24
"github.com/lf-edge/eve/pkg/pillar/utils"
25
+ uuid "github.com/satori/go.uuid"
22
26
"github.com/sirupsen/logrus"
27
+ "google.golang.org/protobuf/encoding/protojson"
23
28
)
24
29
25
30
// dockerAPIPort - unencrypted docker socket for remote password-less access
@@ -28,6 +33,19 @@ const dockerAPIPort int = 2375
28
33
// dockerAPIVersion - docker API version used
29
34
const dockerAPIVersion string = "1.40"
30
35
36
+ const (
37
+ // See detail in https://github.com/lf-edge/eve-tools/blob/master/runtimemetrics/README.md
38
+
39
+ // nestedAppDomainAppPort - TCP port for nested domain app stats for runtime to provide app list and metrics
40
+ // this is used in the http://<runtime-ip>:57475, where the <runtime-ip> is the 'GetStatsIPAddr' of the AppNetworkStatus
41
+ // and the deployment type is 'Docker'
42
+ nestedAppDomainAppPort int = 57475
43
+ // nestedAppDomainAppListURL - URL to get nested domain app list
44
+ nestedAppDomainAppListURL = "/api/v1/inventory/nested-app-id"
45
+ // nestedAppDomainAppMetricsURL - URL to get nested domain app metrics with nested-app uuid
46
+ nestedAppDomainAppMetricsURL = "/api/v1/metrics/nested-app-id/"
47
+ )
48
+
31
49
// check if we need to launch the goroutine to collect App container stats
32
50
func (z * zedrouter ) checkAppContainerStatsCollecting (config * types.AppNetworkConfig ,
33
51
status * types.AppNetworkStatus ) {
@@ -36,11 +54,13 @@ func (z *zedrouter) checkAppContainerStatsCollecting(config *types.AppNetworkCon
36
54
if config != nil {
37
55
if ! status .GetStatsIPAddr .Equal (config .GetStatsIPAddr ) {
38
56
status .GetStatsIPAddr = config .GetStatsIPAddr
57
+ status .DeploymentType = config .DeploymentType
39
58
changed = true
40
59
}
41
60
} else {
42
61
if status .GetStatsIPAddr != nil {
43
62
status .GetStatsIPAddr = nil
63
+ status .DeploymentType = types .AppRuntimeTypeUnSpecified
44
64
changed = true
45
65
}
46
66
}
@@ -72,28 +92,16 @@ func (z *zedrouter) collectAppContainerStats() {
72
92
collectTime := time .Now () // all apps collection assign the same timestamp
73
93
for _ , st := range items {
74
94
status := st .(types.AppNetworkStatus )
95
+ // When the GetStatsIPAddr is configured, we need to handle collecting stats
96
+ // for various deployment types, defined by the DeploymentType. At this moment,
97
+ // we have two types of deployment for stats collection: Docker-Compose and IoT-Edge.
75
98
if status .GetStatsIPAddr != nil {
76
- // get a list of containers and client handle
77
- cli , containers , err := z .getAppContainers (status )
78
- if err != nil {
79
- z .log .Errorf (
80
- "collectAppContainerStats: can't get App Containers %s on %s, %v" ,
81
- status .UUIDandVersion .UUID .String (), status .GetStatsIPAddr .String (),
82
- err )
83
- continue
84
- }
85
- acNum += len (containers )
86
-
87
- // collect container stats, and publish to zedclient
88
- acMetrics := z .getAppContainerStats (cli , containers )
89
- if len (acMetrics .StatsList ) > 0 {
90
- acMetrics .UUIDandVersion = status .UUIDandVersion
91
- acMetrics .CollectTime = collectTime
92
- z .pubAppContainerStats .Publish (acMetrics .Key (), acMetrics )
99
+ switch status .DeploymentType {
100
+ case types .AppRuntimeTypeDocker :
101
+ z .getNestedDomainAppMetrics (status , & acNum )
102
+ default :
103
+ z .getIotEdgeMetricsAndLogs (status , collectTime , lastLogTime , & acNum , & numlogs )
93
104
}
94
-
95
- // collect container logs and send through the logging system
96
- numlogs += z .getAppContainerLogs (status , lastLogTime , cli , containers )
97
105
}
98
106
}
99
107
// log output every 5 min, see this goroutine running status and number
@@ -328,3 +336,185 @@ func (z *zedrouter) getAppContainers(status types.AppNetworkStatus) (
328
336
329
337
return cli , containers , nil
330
338
}
339
+
340
+ // getIotEdgeMetricsAndLogs collects the metrics and logs for IoT-Edge
341
+ func (z * zedrouter ) getIotEdgeMetricsAndLogs (status types.AppNetworkStatus ,
342
+ collectTime time.Time , lastLogTime map [string ]time.Time , acNum , numlogs * int ) {
343
+ // get a list of containers and client handle
344
+ cli , containers , err := z .getAppContainers (status )
345
+ if err != nil {
346
+ z .log .Errorf (
347
+ "getIotEdgeMetricsAndLogs: can't get App Containers %s on %s, %v" ,
348
+ status .UUIDandVersion .UUID .String (), status .GetStatsIPAddr .String (),
349
+ err )
350
+ return
351
+ }
352
+ * acNum += len (containers )
353
+
354
+ // collect container stats, and publish to zedclient
355
+ acMetrics := z .getAppContainerStats (cli , containers )
356
+ if len (acMetrics .StatsList ) > 0 {
357
+ acMetrics .UUIDandVersion = status .UUIDandVersion
358
+ acMetrics .CollectTime = collectTime
359
+ z .pubAppContainerStats .Publish (acMetrics .Key (), acMetrics )
360
+ }
361
+
362
+ // collect container logs and send through the logging system
363
+ * numlogs += z .getAppContainerLogs (status , lastLogTime , cli , containers )
364
+ }
365
+
366
+ // Helper function to construct the URL for nested app operations
367
+ func buildNestedAppURL (status types.AppNetworkStatus , endpoint string , appID string ) string {
368
+ baseURL := fmt .Sprintf ("http://%s:%d%s" , status .GetStatsIPAddr .String (), nestedAppDomainAppPort , endpoint )
369
+ if appID != "" {
370
+ return baseURL + appID
371
+ }
372
+ return baseURL
373
+ }
374
+
375
+ // getNestedDomainAppMetrics collects the metrics for nested domain apps
376
+ // this does several tasks:
377
+ // - http request to runtime agent to get the list of nested domain apps
378
+ // - publish the nested domain apps, currently it can be used by 'newlogd'
379
+ // - http request to runtime agent to get the metrics for each nested domain app
380
+ // - publish the metrics to zedagent w/ types.AppContainerStats
381
+ func (z * zedrouter ) getNestedDomainAppMetrics (status types.AppNetworkStatus , acNum * int ) {
382
+ // first get the list of nested domain apps
383
+ nestedApps , err := z .getNestedDomainAppList (status )
384
+ if err != nil {
385
+ z .log .Errorf ("getNestedDomainAppMetrics: failed to get nested app list, error: %v" , err )
386
+ return
387
+ }
388
+
389
+ * acNum += len (nestedApps )
390
+ var acMetrics types.AppContainerMetrics
391
+ acMetrics .UUIDandVersion = status .UUIDandVersion
392
+ acMetrics .CollectTime = time .Now ()
393
+
394
+ // for each nested domain app, get the metrics
395
+ // this list of nested app metrics is published to zedclient
396
+ // and to be uploaded to the controller along with the runtime or parent app metrics
397
+ for _ , nestedApp := range nestedApps {
398
+ url := buildNestedAppURL (status , nestedAppDomainAppMetricsURL , nestedApp .UUIDandVersion .UUID .String ())
399
+
400
+ data , err := fetchHTTPData (url )
401
+ if err != nil {
402
+ z .log .Errorf ("getNestedDomainAppMetrics: %v" , err )
403
+ continue
404
+ }
405
+
406
+ var nastat nestedapp.NestedAppMetrics
407
+ if err := protojson .Unmarshal (data , & nastat ); err != nil {
408
+ z .log .Errorf ("getNestedDomainAppMetrics: failed to decode JSON data, error: %v" , err )
409
+ continue
410
+ }
411
+
412
+ acStats := types.AppContainerStats {
413
+ ContainerName : nastat .Id ,
414
+ Status : nastat .Status ,
415
+ Pids : nastat .Pids ,
416
+ Uptime : nastat .Uptime ,
417
+ CPUTotal : nastat .CPUTotal ,
418
+ SystemCPUTotal : nastat .SystemCPUTotal ,
419
+ UsedMem : nastat .UsedMem ,
420
+ AllocatedMem : nastat .AllocatedMem ,
421
+ TxBytes : nastat .TxBytes ,
422
+ RxBytes : nastat .RxBytes ,
423
+ ReadBytes : nastat .ReadBytes ,
424
+ WriteBytes : nastat .WriteBytes ,
425
+ }
426
+ acMetrics .StatsList = append (acMetrics .StatsList , acStats )
427
+ }
428
+ // send for zedagent to pack w/ parent app metrics
429
+ z .pubAppContainerStats .Publish (acMetrics .Key (), acMetrics )
430
+
431
+ z .log .Functionf ("getNestedDomainAppMetrics: collected metrics %+v" , acMetrics )
432
+ }
433
+
434
+ // getNestedDomainAppList gets the list of nested domain apps
435
+ func (z * zedrouter ) getNestedDomainAppList (status types.AppNetworkStatus ) ([]types.NestedAppDomainStatus , error ) {
436
+ pub := z .pubNestedAppDomainStatus
437
+ existingItems := pub .GetAll ()
438
+ existingNestedApps := make (map [string ]types.NestedAppDomainStatus )
439
+
440
+ // Save existing items for later comparison
441
+ for _ , st := range existingItems {
442
+ nestedApp := st .(types.NestedAppDomainStatus )
443
+ existingNestedApps [nestedApp .UUIDandVersion .UUID .String ()] = nestedApp
444
+ }
445
+
446
+ // Get the JSON data from the Runtime endpoint
447
+ url := buildNestedAppURL (status , nestedAppDomainAppListURL , "" )
448
+
449
+ data , err := fetchHTTPData (url )
450
+ if err != nil {
451
+ z .log .Errorf ("getNestedDomainAppMetrics: %v" , err )
452
+ return nil , err
453
+ }
454
+
455
+ var nestedAppInventory nestedapp.NestedAppInventory
456
+ // Decode the JSON data into the protobuf struct
457
+ if err := protojson .Unmarshal (data , & nestedAppInventory ); err != nil {
458
+ z .log .Errorf ("getNestedAppListAndMetrics: failed to decode JSON data using protojson, error: %v" , err )
459
+ return nil , err
460
+ }
461
+
462
+ // Process the nested app IDs
463
+ var nestedapps []types.NestedAppDomainStatus
464
+ newNestedApps := make (map [string ]types.NestedAppDomainStatus )
465
+ for _ , nestedAppID := range nestedAppInventory .Apps {
466
+ nestedAppUUID , err := uuid .FromString (nestedAppID .AppId )
467
+ if err != nil {
468
+ z .log .Errorf ("getNestedAppListAndMetrics: invalid UUID %s, error: %v" , nestedAppID .AppId , err )
469
+ continue
470
+ }
471
+
472
+ nestedApp := types.NestedAppDomainStatus {
473
+ UUIDandVersion : types.UUIDandVersion {UUID : nestedAppUUID },
474
+ DisplayName : nestedAppID .AppName ,
475
+ DisableLogs : nestedAppID .DisableLogs ,
476
+ ParentAppUUID : status .UUIDandVersion .UUID ,
477
+ }
478
+
479
+ newNestedApps [nestedAppID .AppId ] = nestedApp
480
+ nestedapps = append (nestedapps , nestedApp )
481
+ }
482
+
483
+ // Compare old and new sets of nested apps and publish if different
484
+ for uuidStr , newNestedApp := range newNestedApps {
485
+ if existingNestedApp , exists := existingNestedApps [uuidStr ]; ! exists || ! cmp .Equal (existingNestedApp , newNestedApp ) {
486
+ z .log .Functionf ("getNestedAppListAndMetrics: publish nestedApp %+v" , newNestedApp )
487
+ z .pubNestedAppDomainStatus .Publish (newNestedApp .Key (), newNestedApp )
488
+ }
489
+ }
490
+
491
+ // handle removed nested apps
492
+ for uuidStr := range existingNestedApps {
493
+ if _ , exists := newNestedApps [uuidStr ]; ! exists {
494
+ z .log .Functionf ("getNestedAppListAndMetrics: remove nestedApp with UUID %s" , uuidStr )
495
+ z .pubNestedAppDomainStatus .Unpublish (uuidStr )
496
+ }
497
+ }
498
+
499
+ return nestedapps , nil
500
+ }
501
+
502
+ // fetchHTTPData fetches data from the given URL
503
+ func fetchHTTPData (url string ) ([]byte , error ) {
504
+ resp , err := http .Get (url )
505
+ if err != nil {
506
+ return nil , fmt .Errorf ("failed to fetch data from %s: %w" , url , err )
507
+ }
508
+ defer resp .Body .Close ()
509
+
510
+ if resp .StatusCode != http .StatusOK {
511
+ return nil , fmt .Errorf ("unexpected status code %d from %s" , resp .StatusCode , url )
512
+ }
513
+
514
+ data , err := io .ReadAll (resp .Body )
515
+ if err != nil {
516
+ return nil , fmt .Errorf ("failed to read response body from %s: %w" , url , err )
517
+ }
518
+
519
+ return data , nil
520
+ }
0 commit comments