@@ -3,6 +3,7 @@ package utils
33import (
44 "context"
55 "fmt"
6+ "strings"
67
78 "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
89 "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
@@ -149,3 +150,72 @@ func CompareResourcesInLogsAndKubeAPI(K8sClient *kubernetes.Clientset, logsClien
149150
150151 return CompareResourcesHelper (logsClient , resourceID , query , resources )
151152}
153+
154+ // QueryContainerLogV2CountsByComputer queries the Log Analytics workspace for
155+ // the number of ContainerLogV2 rows ingested per node (Computer) within the
156+ // given time window (e.g. "5m"). Returns a map keyed by lowercased Computer
157+ // name.
158+ //
159+ // ContainerLogV2 and ContainerLog are mutually exclusive — a cluster writes
160+ // to one or the other based on its schema configuration. This helper only
161+ // falls back to ContainerLog when the ContainerLogV2 query *errors* (e.g.
162+ // the V2 table does not exist in a V1-configured workspace). A successful V2
163+ // query that returns zero rows is treated as a real ingestion failure and
164+ // surfaced as an empty map; callers must NOT interpret that as a reason to
165+ // fall back, otherwise V2 ingestion failures would be silently masked.
166+ func QueryContainerLogV2CountsByComputer (logsClient * azquery.LogsClient , resourceID string , window string ) (map [string ]int64 , error ) {
167+ counts , v2Err := queryCountsByComputer (logsClient , resourceID , "ContainerLogV2" , window )
168+ if v2Err == nil {
169+ return counts , nil
170+ }
171+
172+ fallback , fbErr := queryCountsByComputer (logsClient , resourceID , "ContainerLog" , window )
173+ if fbErr != nil {
174+ return nil , fmt .Errorf ("ContainerLogV2 query failed: %v; ContainerLog fallback failed: %v" , v2Err , fbErr )
175+ }
176+ return fallback , nil
177+ }
178+
179+ func queryCountsByComputer (logsClient * azquery.LogsClient , resourceID string , table string , window string ) (map [string ]int64 , error ) {
180+ query := fmt .Sprintf ("%s | where TimeGenerated > ago(%s) | summarize count() by Computer" , table , window )
181+ tables , err := QueryLogs (logsClient , resourceID , query )
182+ if err != nil {
183+ return nil , err
184+ }
185+
186+ counts := map [string ]int64 {}
187+ for _ , t := range tables {
188+ for _ , row := range t .Rows {
189+ if len (row ) < 2 {
190+ continue
191+ }
192+ computer , ok := row [0 ].(string )
193+ if ! ok || computer == "" {
194+ continue
195+ }
196+ count , _ := row [1 ].(float64 )
197+ counts [strings .ToLower (computer )] += int64 (count )
198+ }
199+ }
200+ return counts , nil
201+ }
202+
203+ // AssertContainerLogV2NodeCoverage returns nil if every expected node appears
204+ // in the per-Computer count map with a positive row count (compared
205+ // case-insensitively), or an error listing the missing nodes otherwise.
206+ func AssertContainerLogV2NodeCoverage (expectedNodes []string , observedCountsByComputer map [string ]int64 ) error {
207+ if len (expectedNodes ) == 0 {
208+ return fmt .Errorf ("no expected nodes provided; cannot verify ContainerLogV2 coverage" )
209+ }
210+
211+ var missing []string
212+ for _ , n := range expectedNodes {
213+ if observedCountsByComputer [strings .ToLower (n )] <= 0 {
214+ missing = append (missing , n )
215+ }
216+ }
217+ if len (missing ) > 0 {
218+ return fmt .Errorf ("ContainerLogV2 ingestion is missing for %d/%d expected node(s): %s" , len (missing ), len (expectedNodes ), strings .Join (missing , ", " ))
219+ }
220+ return nil
221+ }
0 commit comments