Esempi per query Kusto

Questo articolo identifica le query comuni e come è possibile usare il Linguaggio di query Kusto per soddisfarli.

Visualizzare un istogramma

Per proiettare due o più colonne e quindi usarle come asse x e asse y di un grafico:

StormEvents
| where isnotempty(EndLocation) 
| summarize event_count=count() by EndLocation
| top 10 by event_count
| render columnchart
  • La prima colonna forma l'asse x. Può essere di tipo numerico, data/ora o stringa.
  • Usare where, summarize e top per limitare la quantità di dati visualizzati.
  • Disporre i risultati per definire l'ordine dell'asse x.

Screenshot of a column chart, with ten colored columns that depict the respective values of 10 locations.

Individuare le sessioni in base agli eventi start e stop

In un log eventi alcuni eventi contrassegnano l'inizio o la fine di un'attività o di una sessione estesa.

Nome city SessionId Timestamp
Avvio Londra 2817330 2015-12-09T10:12:02.32
Gioco Londra 2817330 2015-12-09T10:12:52.45
Avvio Manchester 4267667 2015-12-09T10:14:02.23
Interrompere Londra 2817330 2015-12-09T10:23:43.18
Annulla Manchester 4267667 2015-12-09T10:27:26.29
Interrompere Manchester 4267667 2015-12-09T10:28:31.72

Ogni evento è associato a un ID sessione (SessionId). Il problema è associare gli eventi di inizio e di fine con un ID sessione.

Esempio:

let Events = MyLogTable | where ... ;
Events
| where Name == "Start"
| project Name, City, SessionId, StartTime=timestamp
| join (Events 
        | where Name="Stop"
        | project StopTime=timestamp, SessionId) 
    on SessionId
| project City, SessionId, StartTime, StopTime, Duration = StopTime - StartTime

Per associare gli eventi di inizio e di fine con un ID sessione:

  1. Usare let per assegnare un nome a una proiezione della tabella semplificata il più possibile prima di avviare il join.
  2. Usare project per cambiare i nomi dei timestamp in modo che nei risultati vengano visualizzate sia l'ora di inizio che di fine. project seleziona inoltre le altre colonne da visualizzare nei risultati.
  3. Usare join per associare le voci di avvio e di arresto per la stessa attività. Per ogni attività viene creata una riga.
  4. Usare project di nuovo per aggiungere una colonna che mostri la durata dell'attività.

Di seguito è riportato l'output generato:

City SessionId StartTime StopTime Duration
Londra 2817330 2015-12-09T10:12:02.32 2015-12-09T10:23:43.18 00:11:40.46
Manchester 4267667 2015-12-09T10:14:02.23 2015-12-09T10:28:31.72 00:14:29.49

Ottenere sessioni senza usare un ID sessione

Si supponga che gli eventi di avvio e arresto non includano un ID sessione con cui trovare una corrispondenza. È tuttavia disponibile l'indirizzo IP del client in cui è stata eseguita la sessione. Supponendo che ogni indirizzo del client punti a una sola sessione alla volta, è possibile abbinare ogni evento di avvio al successivo evento di arresto proveniente dallo stesso indirizzo IP:

Esempio:

Events 
| where Name == "Start" 
| project City, ClientIp, StartTime = timestamp
| join  kind=inner
    (Events
    | where Name == "Stop" 
    | project StopTime = timestamp, ClientIp)
    on ClientIp
| extend duration = StopTime - StartTime 
    // Remove matches with earlier stops:
| where  duration > 0  
    // Pick out the earliest stop for each start and client:
| summarize arg_min(duration, *) by bin(StartTime,1s), ClientIp

join associa ogni ora di avvio con tutte le ore di arresto provenienti dallo stesso indirizzo IP del client. Il codice di esempio:

  • Rimuove le corrispondenze con le ore di arresto precedenti.
  • Esegue un raggruppamento in base all'ora di avvio e all'indirizzo IP per ottenere un gruppo per ogni sessione.
  • Fornisce una funzione bin per il parametro StartTime. Se questo passaggio non viene eseguito, Kusto usa automaticamente bin di un'ora che associano alcune ore di avvio con le ore di arresto errate.

arg_min trova la riga con la durata minima in ogni gruppo e il parametro * passa questo valore a tutte le altre colonne.

L'argomento aggiunge il prefisso min_ a ogni nome di colonna.

Screenshot of a table that lists the results, with columns for the start time, client IP, duration, city, and earliest stop for each client/start time combination.

Aggiungere codice per contare le durate in bin di dimensioni appropriate. In questo esempio, poiché si preferisce usare il grafico a barre, si esegue la divisione per 1s per convertire gli intervalli di tempo in numeri:

    // Count the frequency of each duration:
    | summarize count() by duration=bin(min_duration/1s, 10) 
      // Cut off the long tail:
    | where duration < 300
      // Display in a bar chart:
    | sort by duration asc | render barchart 

Screenshot of a column chart that depicts the number of sessions, with durations in specified ranges.

Esempio completo

Logs  
| filter ActivityId == "ActivityId with Blablabla" 
| summarize max(Timestamp), min(Timestamp)  
| extend Duration = max_Timestamp - min_Timestamp 

wabitrace  
| filter Timestamp >= datetime(2015-01-12 11:00:00Z)  
| filter Timestamp < datetime(2015-01-12 13:00:00Z)  
| filter EventText like "NotifyHadoopApplicationJobPerformanceCounters"  	 
| extend Tenant = extract("tenantName=([^,]+),", 1, EventText) 
| extend Environment = extract("environmentName=([^,]+),", 1, EventText)  
| extend UnitOfWorkId = extract("unitOfWorkId=([^,]+),", 1, EventText)  
| extend TotalLaunchedMaps = extract("totalLaunchedMaps=([^,]+),", 1, EventText, typeof(real))  
| extend MapsSeconds = extract("mapsMilliseconds=([^,]+),", 1, EventText, typeof(real)) / 1000 
| extend TotalMapsSeconds = MapsSeconds  / TotalLaunchedMaps 
| filter Tenant == 'DevDiv' and Environment == 'RollupDev2'  
| filter TotalLaunchedMaps > 0 
| summarize sum(TotalMapsSeconds) by UnitOfWorkId  
| extend JobMapsSeconds = sum_TotalMapsSeconds * 1 
| project UnitOfWorkId, JobMapsSeconds 
| join ( 
wabitrace  
| filter Timestamp >= datetime(2015-01-12 11:00:00Z)  
| filter Timestamp < datetime(2015-01-12 13:00:00Z)  
| filter EventText like "NotifyHadoopApplicationJobPerformanceCounters"  
| extend Tenant = extract("tenantName=([^,]+),", 1, EventText) 
| extend Environment = extract("environmentName=([^,]+),", 1, EventText)  
| extend UnitOfWorkId = extract("unitOfWorkId=([^,]+),", 1, EventText)   
| extend TotalLaunchedReducers = extract("totalLaunchedReducers=([^,]+),", 1, EventText, typeof(real)) 
| extend ReducesSeconds = extract("reducesMilliseconds=([^,]+)", 1, EventText, typeof(real)) / 1000 
| extend TotalReducesSeconds = ReducesSeconds / TotalLaunchedReducers 
| filter Tenant == 'DevDiv' and Environment == 'RollupDev2'  
| filter TotalLaunchedReducers > 0 
| summarize sum(TotalReducesSeconds) by UnitOfWorkId  
| extend JobReducesSeconds = sum_TotalReducesSeconds * 1 
| project UnitOfWorkId, JobReducesSeconds ) 
on UnitOfWorkId 
| join ( 
wabitrace  
| filter Timestamp >= datetime(2015-01-12 11:00:00Z)  
| filter Timestamp < datetime(2015-01-12 13:00:00Z)  
| filter EventText like "NotifyHadoopApplicationJobPerformanceCounters"  
| extend Tenant = extract("tenantName=([^,]+),", 1, EventText) 
| extend Environment = extract("environmentName=([^,]+),", 1, EventText)  
| extend JobName = extract("jobName=([^,]+),", 1, EventText)  
| extend StepName = extract("stepName=([^,]+),", 1, EventText)  
| extend UnitOfWorkId = extract("unitOfWorkId=([^,]+),", 1, EventText)  
| extend LaunchTime = extract("launchTime=([^,]+),", 1, EventText, typeof(datetime))  
| extend FinishTime = extract("finishTime=([^,]+),", 1, EventText, typeof(datetime)) 
| extend TotalLaunchedMaps = extract("totalLaunchedMaps=([^,]+),", 1, EventText, typeof(real))  
| extend TotalLaunchedReducers = extract("totalLaunchedReducers=([^,]+),", 1, EventText, typeof(real)) 
| extend MapsSeconds = extract("mapsMilliseconds=([^,]+),", 1, EventText, typeof(real)) / 1000 
| extend ReducesSeconds = extract("reducesMilliseconds=([^,]+)", 1, EventText, typeof(real)) / 1000 
| extend TotalMapsSeconds = MapsSeconds  / TotalLaunchedMaps  
| extend TotalReducesSeconds = (ReducesSeconds / TotalLaunchedReducers / ReducesSeconds) * ReducesSeconds  
| extend CalculatedDuration = (TotalMapsSeconds + TotalReducesSeconds) * time(1s) 
| filter Tenant == 'DevDiv' and Environment == 'RollupDev2') 
on UnitOfWorkId 
| extend MapsFactor = TotalMapsSeconds / JobMapsSeconds 
| extend ReducesFactor = TotalReducesSeconds / JobReducesSeconds 
| extend CurrentLoad = 1536 + (768 * TotalLaunchedMaps) + (1536 * TotalLaunchedMaps) 
| extend NormalizedLoad = 1536 + (768 * TotalLaunchedMaps * MapsFactor) + (1536 * TotalLaunchedMaps * ReducesFactor) 
| summarize sum(CurrentLoad), sum(NormalizedLoad) by  JobName  
| extend SaveFactor = sum_NormalizedLoad / sum_CurrentLoad 

Rappresentare sessioni simultanee nel tempo in un grafico

Si supponga di avere una tabella di attività con le relative ore di inizio e di fine. È possibile mostrare un grafico che visualizza il numero di attività eseguite simultaneamente nel corso del tempo.

Ecco un input di esempio denominato X:

SessionId StartTime StopTime
a 10:01:03 10:10:08
b 10:01:29 10:03:10
c 10:03:02 10:05:20

Per un grafico con bin di un minuto, è opportuno contare ogni attività in esecuzione in corrispondenza di ogni intervallo di un minuto.

Di seguito è riportato un risultato intermedio:

X | extend samples = range(bin(StartTime, 1m), StopTime, 1m)

range genera una matrice di valori agli intervalli specificati:

SessionId StartTime StopTime esempi
a 10:01:33 10:06:31 [10:01:00,10:02:00,...10:06:00]
b 10:02:29 10:03:45 [10:02:00,10:03:00]
c 10:03:12 10:04:30 [10:03:00,10:04:00]

Invece di mantenere queste matrici, espanderle usando mv-expand:

X | mv-expand samples = range(bin(StartTime, 1m), StopTime , 1m)
SessionId StartTime StopTime esempi
a 10:01:33 10:06:31 10:01:00
a 10:01:33 10:06:31 10:02:00
a 10:01:33 10:06:31 10:03:00
a 10:01:33 10:06:31 10:04:00
a 10:01:33 10:06:31 10:05:00
a 10:01:33 10:06:31 10:06:00
b 10:02:29 10:03:45 10:02:00
b 10:02:29 10:03:45 10:03:00
c 10:03:12 10:04:30 10:03:00
c 10:03:12 10:04:30 10:04:00

Ora raggruppare i risultati in base all'ora e contare le occorrenze di ogni attività:

X
| mv-expand samples = range(bin(StartTime, 1m), StopTime , 1m)
| summarize count_SessionId = count() by bin(todatetime(samples),1m)
  • Usare todatetime() perché mv-expand genera una colonna di tipo dinamico.
  • Usare bin() perché, per valori numerici e date, se non si specifica un intervallo, summarize applica sempre una funzione bin() usando un intervallo predefinito.

Di seguito è riportato l'output generato:

count_SessionId esempi
1 10:01:00
2 10:02:00
3 10:03:00
2 10:04:00
1 10:05:00
1 10:06:00

È possibile usare un grafico a barre o un diagramma temporale per visualizzare i risultati.

Introdurre bin Null in summarize

Quando l'operatore summarize viene applicato alla chiave di un gruppo che consiste in una colonna data/ora, dividere tali valori in bin di larghezza fissa:

let StartTime=ago(12h);
let StopTime=now()
T
| where Timestamp > StartTime and Timestamp <= StopTime 
| where ...
| summarize Count=count() by bin(Timestamp, 5m)

Questo esempio produce una tabella con una singola riga per ogni gruppo di righe in T che rientrano in ogni bin di cinque minuti.

Il codice non aggiunge però bin Null, ossia righe per i valori di bin temporali compresi tra StartTime e StopTime per cui non esiste una riga corrispondente in T. È consigliabile riempire la tabella con tali bin. Ecco un modo per eseguire questa operazione:

let StartTime=ago(12h);
let StopTime=now()
T
| where Timestamp > StartTime and Timestamp <= StopTime 
| summarize Count=count() by bin(Timestamp, 5m)
| where ...
| union ( // 1
  range x from 1 to 1 step 1 // 2
  | mv-expand Timestamp=range(StartTime, StopTime, 5m) to typeof(datetime) // 3
  | extend Count=0 // 4
  )
| summarize Count=sum(Count) by bin(Timestamp, 5m) // 5 

Ecco una spiegazione dettagliata della query precedente:

  1. Usare l'operatore union per aggiungere altre righe a una tabella. Queste righe vengono prodotte dall'espressione union.
  2. L'operatore range produce una tabella con una singola riga e una singola colonna. La tabella viene usata esclusivamente per le operazioni di mv-expand.
  3. L'operatore mv-expand sulla funzione range crea il numero di righe corrispondente al numero di bin di cinque minuti compresi tra StartTime e EndTime.
  4. Usare un valore 0 per Count.
  5. L'operatore summarize raggruppa i bin dell'argomento originale (left, ovvero outer) in union. L'operatore crea inoltre bin dall'argomento inner a questo (le righe di bin Null). Questo processo assicura che l'output abbia un'unica riga per ogni bin il cui valore è pari a zero o al conteggio originale.

Ottenere di più dai dati usando Kusto con Machine Learning

Molti casi d'uso interessanti prevedono l'uso di algoritmi di Machine Learning e il recupero di informazioni dettagliate significative dai dati di telemetria. Spesso questi algoritmi richiedono un set di dati strutturato rigorosamente come input. I dati di log non elaborati non soddisfano i requisiti di struttura e dimensioni.

Per iniziare, individuare le anomalie nella percentuale di errori di uno specifico servizio di inferenze di Bing. La tabella di log include 65 miliardi di record. La query di base seguente filtra 250.000 errori e quindi crea una serie temporale del conteggio di errori che usa la funzione di rilevamento anomalie series_decompose_anomalies. Le anomalie vengono rilevate dal servizio Kusto e vengono evidenziate come punti rossi nel grafico della serie temporale.

Logs
| where Timestamp >= datetime(2015-08-22) and Timestamp < datetime(2015-08-23) 
| where Level == "e" and Service == "Inferences.UnusualEvents_Main" 
| summarize count() by bin(Timestamp, 5min)
| render anomalychart 

Il servizio ha identificato alcuni bucket temporali che presentano percentuali di errori sospette. Usare Kusto per ingrandire questo intervallo di tempo. Quindi eseguire una query che crea un'aggregazione in base alla colonna Message. Provare a individuare gli errori principali.

Le parti pertinenti dell'intera analisi dello stack del messaggio vengono tagliate, in modo che i risultati rientrino nella pagina.

È possibile vedere l'identificazione corretta dei primi otto errori. Tuttavia, in seguito è riportata una lunga serie di errori, perché il messaggio di errore è stato creato usando una stinga di formato che conteneva dati mutevoli:

Logs
| where Timestamp >= datetime(2015-08-22 05:00) and Timestamp < datetime(2015-08-22 06:00)
| where Level == "e" and Service == "Inferences.UnusualEvents_Main"
| summarize count() by Message 
| top 10 by count_ 
| project count_, Message 
count_ Message
7125 ExecuteAlgorithmMethod per il metodo 'RunCycleFromInterimData' non riuscita...
7125 Chiamata a InferenceHostService non riuscita..System.NullReferenceException: Riferimento a un oggetto non impostato su un'istanza di un oggetto...
7124 Errore imprevisto del sistema di inferenze..System.NullReferenceException: Riferimento a un oggetto non impostato su un'istanza di un oggetto...
5112 Errore imprevisto del sistema di inferenze..System.NullReferenceException: Riferimento a un oggetto non impostato su un'istanza di un oggetto...
174 Chiamata a InferenceHostService non riuscita..System.ServiceModel.CommunicationException: Errore durante la scrittura nella pipe:...
10 ExecuteAlgorithmMethod per il metodo 'RunCycleFromInterimData' non riuscita...
10 Errore del sistema di inferenze..Microsoft.Bing.Platform.Inferences.Service.Managers.UserInterimDataManagerException:...
3 Chiamata a InferenceHostService non riuscita..System.ServiceModel.CommunicationObjectFaultedException:...
1 Errore del sistema di inferenze... SocialGraph.BOSS.OperationResponse...AIS TraceId:8292FC561AC64BED8FA243808FE74EFD...
1 Errore del sistema di inferenze... SocialGraph.BOSS.OperationResponse...AIS TraceId: 5F79F7587FF943EC9B641E02E701AFBF...

A questo punto, risulta utile usare l'operatore reduce. L'operatore ha identificato 63 errori diversi originati nello stesso punto di strumentazione dell'analisi nel codice. reduce consente di concentrarsi su altre tracce di errore significative in questa finestra temporale.

Logs
| where Timestamp >= datetime(2015-08-22 05:00) and Timestamp < datetime(2015-08-22 06:00)
| where Level == "e" and Service == "Inferences.UnusualEvents_Main"
| reduce by Message with threshold=0.35
| project Count, Pattern
Conteggio Modello
7125 ExecuteAlgorithmMethod per il metodo 'RunCycleFromInterimData' non riuscita...
7125 Chiamata a InferenceHostService non riuscita..System.NullReferenceException: Riferimento a un oggetto non impostato su un'istanza di un oggetto...
7124 Errore imprevisto del sistema di inferenze..System.NullReferenceException: Riferimento a un oggetto non impostato su un'istanza di un oggetto...
5112 Errore imprevisto del sistema di inferenze..System.NullReferenceException: Riferimento a un oggetto non impostato su un'istanza di un oggetto...
174 Chiamata a InferenceHostService non riuscita..System.ServiceModel.CommunicationException: Errore durante la scrittura nella pipe:...
63 Errore del sistema di inferenza.. Microsoft. Bing. Platform.Inferences.*: Scrivere * per scrivere in Object BOSS.*: SocialGraph.BOSS.Reques...
10 ExecuteAlgorithmMethod per il metodo 'RunCycleFromInterimData' non riuscita...
10 Errore del sistema di inferenze..Microsoft.Bing.Platform.Inferences.Service.Managers.UserInterimDataManagerException:...
3 Chiamata InferenceHostService non riuscita. System.ServiceModel.*: l'oggetto System.ServiceModel.Channels.*+*, per ** è *... in Syst...

A questo punto è disponibile una visualizzazione valida dei principali errori che hanno contribuito alle anomalie rilevate.

Per capire l'effetto di questi errori nel sistema di esempio, considerare che:

  • La tabella Logs contiene altri dati dimensionali, come Component e Cluster.
  • Il nuovo plug-in autocluster consente di derivare informazioni dettagliate su componenti e cluster con una semplice query.

Nell'esempio seguente è possibile vedere chiaramente che ognuno dei primi quattro errori è specifico di un componente. Inoltre, anche se i primi tre errori sono specifici del cluster DB4, il quarto si verifica in tutti i cluster.

Logs
| where Timestamp >= datetime(2015-08-22 05:00) and Timestamp < datetime(2015-08-22 06:00)
| where Level == "e" and Service == "Inferences.UnusualEvents_Main"
| evaluate autocluster()
Conteggio Percentuale (%) Componente Cluster Message
7125 26.64 InferenceHostService DB4 ExecuteAlgorithmMethod per il metodo...
7125 26.64 Componente sconosciuto DB4 Chiamata a InferenceHostService non riuscita...
7124 26.64 InferenceAlgorithmExecutor DB4 Errore imprevisto del sistema di inferenze...
5112 19.11. InferenceAlgorithmExecutor * Errore imprevisto del sistema di inferenze...

Eseguire il mapping dei valori da un set a un altro

Un caso d'uso di query comune è costituito dal mapping statico di valori. Il mapping statico può aiutare a rendere i risultati più presentabili.

Ad esempio, nella tabella seguente DeviceModel specifica un modello di dispositivo. L'uso del modello di dispositivo non risulta particolarmente utile per fare riferimento al nome del dispositivo. 

DeviceModel Conteggio
iPhone5,1 32
iPhone3,2 432
iPhone7,2 55
iPhone5,2 66

 L'uso di un nome descrittivo è più appropriato:

FriendlyName Conteggio
iPhone 5 32
iPhone 4 432
iPhone 6 55
iPhone5 66

I due esempi seguenti dimostrano come passare dall'uso di un modello di dispositivo all'uso di un nome descrittivo per identificare il dispositivo. 

Eseguire il mapping usando un dizionario dinamico

È possibile realizzare il mapping usando un dizionario dinamico e funzioni di accesso dinamiche. Ad esempio:

// Dataset definition
let Source = datatable(DeviceModel:string, Count:long)
[
  'iPhone5,1', 32,
  'iPhone3,2', 432,
  'iPhone7,2', 55,
  'iPhone5,2', 66,
];
// Query start here
let phone_mapping = dynamic(
  {
    "iPhone5,1" : "iPhone 5",
    "iPhone3,2" : "iPhone 4",
    "iPhone7,2" : "iPhone 6",
    "iPhone5,2" : "iPhone5"
  });
Source 
| project FriendlyName = phone_mapping[DeviceModel], Count
FriendlyName Conteggio
iPhone 5 32
iPhone 4 432
iPhone 6 55
iPhone5 66

Eseguire il mapping con una tabella statica

È anche possibile realizzare il mapping usando una tabella persistente e un operatore join.

  1. Creare la tabella di mapping una sola volta:

    .create table Devices (DeviceModel: string, FriendlyName: string) 
    
    .ingest inline into table Devices 
        ["iPhone5,1","iPhone 5"]["iPhone3,2","iPhone 4"]["iPhone7,2","iPhone 6"]["iPhone5,2","iPhone5"]
    
  2. Creare una tabella del contenuto del dispositivo:

    DeviceModel FriendlyName
    iPhone5,1 iPhone 5
    iPhone3,2 iPhone 4
    iPhone7,2 iPhone 6
    iPhone5,2 iPhone5
  3. Creare un'origine di tabella di test:

    .create table Source (DeviceModel: string, Count: int)
    
    .ingest inline into table Source ["iPhone5,1",32]["iPhone3,2",432]["iPhone7,2",55]["iPhone5,2",66]
    
  4. Unire le tabelle ed eseguire il progetto:

    Devices  
    | join (Source) on DeviceModel  
    | project FriendlyName, Count
    

Di seguito è riportato l'output generato:

FriendlyName Conteggio
iPhone 5 32
iPhone 4 432
iPhone 6 55
iPhone5 66

Creare e usare tabelle di dimensioni temporali con una query

Spesso è necessario unire i risultati di una query con una tabella delle dimensioni ad hoc che non è archiviata nel database. È possibile definire un'espressione il cui risultato è un tabella con l'ambito definito in una singola query.

Ad esempio:

// Create a query-time dimension table using datatable
let DimTable = datatable(EventType:string, Code:string)
  [
    "Heavy Rain", "HR",
    "Tornado",    "T"
  ]
;
DimTable
| join StormEvents on EventType
| summarize count() by Code

Ecco un esempio leggermente più complesso:

// Create a query-time dimension table using datatable
let TeamFoundationJobResult = datatable(Result:int, ResultString:string)
  [
    -1, 'None', 0, 'Succeeded', 1, 'PartiallySucceeded', 2, 'Failed',
    3, 'Stopped', 4, 'Killed', 5, 'Blocked', 6, 'ExtensionNotFound',
    7, 'Inactive', 8, 'Disabled', 9, 'JobInitializationError'
  ]
;
JobHistory
  | where PreciseTimeStamp > ago(1h)
  | where Service  != "AX"
  | where Plugin has "Analytics"
  | sort by PreciseTimeStamp desc
  | join kind=leftouter TeamFoundationJobResult on Result
  | extend ExecutionTimeSpan = totimespan(ExecutionTime)
  | project JobName, StartTime, ExecutionTimeSpan, ResultString, ResultMessage

Recuperare gli ultimi record (in base a timestamp) per ogni identità

Si supponga di avere una tabella che include:

  • Una colonna ID che identifica l'entità con cui è associata ogni riga, ad esempio un ID utente o un ID nodo
  • Una colonna timestamp che fornisce il riferimento temporale per la riga
  • Altre colonne

È possibile usare l'operatore top-nested per creare una query che restituisce gli ultimi due record per ogni valore della colonna ID, in cui la definizione del termine ultimi è con il valore massimo ditimestamp :

datatable(id:string, timestamp:datetime, bla:string)           // #1
  [
  "Barak",  datetime(2015-01-01), "1",
  "Barak",  datetime(2016-01-01), "2",
  "Barak",  datetime(2017-01-20), "3",
  "Donald", datetime(2017-01-20), "4",
  "Donald", datetime(2017-01-18), "5",
  "Donald", datetime(2017-01-19), "6"
  ]
| top-nested   of id        by dummy0=max(1),                  // #2
  top-nested 2 of timestamp by dummy1=max(timestamp),          // #3
  top-nested   of bla       by dummy2=max(1)                   // #4
| project-away dummy0, dummy1, dummy2                          // #5

Ecco una spiegazione dettagliata della query precedente (la numerazione fa riferimento ai numeri inclusi nei commenti del codice):

  1. datatable è un modo per produrre alcuni dati di test a scopo dimostrativo. In genere, si usano dati reali.
  2. Questa riga essenzialmente significa restituire tutti i valori distinti di id .
  3. Questa riga quindi restituisce, per i primi due record con i valori massimi:
    • La colonna timestamp.
    • Le colonne del livello precedente, in questo caso solo id
    • La colonna specificata in questo livello, ovvero timestamp
  4. Questa riga aggiunge i valori della colonna bla per ogni record restituito dal livello precedente. Se la tabella include altre colonne di interesse, è possibile ripetere questa riga per ognuna.
  5. La riga finale usa l'operatore project-away per rimuovere le colonne aggiuntive introdotte da top-nested.

Estendere una tabella di una percentuale del calcolo totale

Un'espressione tabulare che include una colonna numerica è più utile all'utente se è accompagnata dal relativo valore come percentuale del totale.

Si supponga ad esempio che una query produca la tabella seguente:

Serie Int
Apple 100
Banana 200

Si vuole che la tabella venga visualizzata in questo modo:

Serie Int %
Apple 100 33,3
Banana 200 66,6

Per cambiare l'aspetto della tabella, calcolare il totale (somma) della colonna SomeInt e quindi dividere ogni valore di questa colonna per il totale. Per risultati arbitrari, usare l'operatore as.

Ad esempio:

// The following table literally represents a long calculation
// that ends up with an anonymous tabular value:
datatable (SomeInt:int, SomeSeries:string) [
  100, "Apple",
  200, "Banana",
]
// We now give this calculation a name ("X"):
| as X
// Having this name we can refer to it in the sub-expression
// "X | summarize sum(SomeInt)":
| extend Pct = 100 * bin(todouble(SomeInt) / toscalar(X | summarize sum(SomeInt)), 0.001)

Eseguire aggregazioni su una finestra temporale scorrevole

L'esempio seguente mostra come riepilogare le colonne usando una finestra temporale scorrevole. Per la query, usare la tabella seguente, che contiene i prezzi della frutta in base a timestamp.

Calcolare i costi minimo, massimo e sommato di ogni frutto per giorno usando una finestra temporale scorrevole di sette giorni. Ogni record nel set di risultati aggrega i sette giorni precedenti e i risultati contengono un record per ogni giorno del periodo di analisi.

Tabella della frutta:

Timestamp Frutta Prezzo
2018-09-24 21:00:00.0000000 Banane 3
2018-09-25 20:00:00.0000000 Mele 9
2018-09-26 03:00:00.0000000 Banane 4
2018-09-27 10:00:00.0000000 Prugne 8
2018-09-28 07:00:00.0000000 Banane 6
2018-09-29 21:00:00.0000000 Banane 8
2018-09-30 01:00:00.0000000 Prugne 2
2018-10-01 05:00:00.0000000 Banane 0
2018-10-02 02:00:00.0000000 Banane 0
2018-10-03 13:00:00.0000000 Prugne 4
2018-10-04 14:00:00.0000000 Mele 8
2018-10-05 05:00:00.0000000 Banane 2
2018-10-06 08:00:00.0000000 Prugne 8
2018-10-07 12:00:00.0000000 Banane 0

Ecco la query di aggregazione della finestra temporale scorrevole. Dopo il risultato della query è riportata la spiegazione.

let _start = datetime(2018-09-24);
let _end = _start + 13d; 
Fruits 
| extend _bin = bin_at(Timestamp, 1d, _start) // #1 
| extend _endRange = iif(_bin + 7d > _end, _end, 
                            iff( _bin + 7d - 1d < _start, _start,
                                iff( _bin + 7d - 1d < _bin, _bin,  _bin + 7d - 1d)))  // #2
| extend _range = range(_bin, _endRange, 1d) // #3
| mv-expand _range to typeof(datetime) limit 1000000 // #4
| summarize min(Price), max(Price), sum(Price) by Timestamp=bin_at(_range, 1d, _start) ,  Fruit // #5
| where Timestamp >= _start + 7d; // #6

Di seguito è riportato l'output generato:

Timestamp Frutta min_Price max_Price sum_Price
2018-10-01 00:00:00.0000000 Mele 9 9 9
2018-10-01 00:00:00.0000000 Banane 0 8 18
2018-10-01 00:00:00.0000000 Prugne 2 8 10
2018-10-02 00:00:00.0000000 Banane 0 8 18
2018-10-02 00:00:00.0000000 Prugne 2 8 10
2018-10-03 00:00:00.0000000 Prugne 2 8 14
2018-10-03 00:00:00.0000000 Banane 0 8 14
2018-10-04 00:00:00.0000000 Banane 0 8 14
2018-10-04 00:00:00.0000000 Prugne 2 4 6
2018-10-04 00:00:00.0000000 Mele 8 8 8
2018-10-05 00:00:00.0000000 Banane 0 8 10
2018-10-05 00:00:00.0000000 Prugne 2 4 6
2018-10-05 00:00:00.0000000 Mele 8 8 8
2018-10-06 00:00:00.0000000 Prugne 2 8 14
2018-10-06 00:00:00.0000000 Banane 0 2 2
2018-10-06 00:00:00.0000000 Mele 8 8 8
2018-10-07 00:00:00.0000000 Banane 0 2 2
2018-10-07 00:00:00.0000000 Prugne 4 8 12
2018-10-07 00:00:00.0000000 Mele 8 8 8

La query duplica ogni record della tabella di input per tutti i sette giorni dopo l'apparizione effettiva. Ogni record compare effettivamente sette volte. Di conseguenza, l'aggregazione giornaliera include tutti i record dei sette giorni precedenti.

Ecco una spiegazione dettagliata della query precedente:

  1. Inserire ogni record i un bin di un giorno, rispetto a _start.
  2. Determinare la fine dell'intervallo per ogni record: _bin + 7d, a meno che il valore non sia incluso nell'intervallo tra _start e _end, nel qual caso viene modificato.
  3. Per ogni record, creare una matrice di setti giorni (timestamp) a partire dal giorno del record corrente.
  4. Usare mv-expand sulla matrice, duplicando così ogni record in sette record, con un giorno di distanza uno dall'altro.
  5. Eseguire la funzione di aggregazione per ogni giorno. A causa del passaggio 4, questo passaggio effettivamente riepiloga gli ultimi sette giorni.
  6. I dati per i primi sette giorni sono incompleti, perché non esiste un periodo di riferimento temporale per tali giorni. I primi sette giorni sono esclusi dal risultato finale. Nell'esempio partecipano solo nell'aggregazione per il timestamp 2018-10-01.

Trovare l'evento precedente

L'esempio seguente dimostra come trovare un evento precedente tra due set di dati.

Esistono due set di dati, A e B. Per ogni record del set di dati B, bisogna trovare l'evento precedente nel set di dati A, ovvero il record arg_max in A che è ancora meno recente di B.

Ecco i set di dati di esempio:

let A = datatable(Timestamp:datetime, ID:string, EventA:string)
[
    datetime(2019-01-01 00:00:00), "x", "Ax1",
    datetime(2019-01-01 00:00:01), "x", "Ax2",
    datetime(2019-01-01 00:00:02), "y", "Ay1",
    datetime(2019-01-01 00:00:05), "y", "Ay2",
    datetime(2019-01-01 00:00:00), "z", "Az1"
];
let B = datatable(Timestamp:datetime, ID:string, EventB:string)
[
    datetime(2019-01-01 00:00:03), "x", "B",
    datetime(2019-01-01 00:00:04), "x", "B",
    datetime(2019-01-01 00:00:04), "y", "B",
    datetime(2019-01-01 00:02:00), "z", "B"
];
A; B
Timestamp ID EventB
2019-01-01 00:00:00.0000000 x Ax1
2019-01-01 00:00:00.0000000 z Az1
2019-01-01 00:00:01.0000000 x Ax2
2019-01-01 00:00:02.0000000 y Ay1
2019-01-01 00:00:05.0000000 y Ay2

Timestamp ID EventA
2019-01-01 00:00:03.0000000 x B
2019-01-01 00:00:04.0000000 x B
2019-01-01 00:00:04.0000000 y B
2019-01-01 00:02:00.0000000 z B

Output previsto:

ID Timestamp EventB A_Timestamp EventA
x 2019-01-01 00:00:03.0000000 B 2019-01-01 00:00:01.0000000 Ax2
x 2019-01-01 00:00:04.0000000 B 2019-01-01 00:00:01.0000000 Ax2
y 2019-01-01 00:00:04.0000000 B 2019-01-01 00:00:02.0000000 Ay1
z 2019-01-01 00:02:00.0000000 B 2019-01-01 00:00:00.0000000 Az1

È consigliabile adottare due approcci diversi per questo problema. È possibile testare entrambi in uno specifico set di dati per trovare quello più adatto per un determinato scenario.

Nota

Ogni approccio può essere eseguito diversamente su set di dati differenti.

Approccio 1

Questo approccio serializza entrami i set di dati in base a ID e timestamp. Quindi raggruppa tutti gli eventi del set di dati B con tutti gli eventi precedenti del set di dati A. Infine, sceglie il valore di arg_max tra tutti gli eventi del set di dati A del gruppo.

A
| extend A_Timestamp = Timestamp, Kind="A"
| union (B | extend B_Timestamp = Timestamp, Kind="B")
| order by ID, Timestamp asc 
| extend t = iff(Kind == "A" and (prev(Kind) != "A" or prev(Id) != ID), 1, 0)
| extend t = row_cumsum(t)
| summarize Timestamp=make_list(Timestamp), EventB=make_list(EventB), arg_max(A_Timestamp, EventA) by t, ID
| mv-expand Timestamp to typeof(datetime), EventB to typeof(string)
| where isnotempty(EventB)
| project-away t

Approccio 2

Questo approccio alla risoluzione del problema richiede un periodo di riferimento temporale massimo. Esamina quanto meno recente può essere il record del set di dati A rispetto a quello del set di dati B. Il metodo quindi unisce i due set di dati in base all'ID e a questo periodo di riferimento temporale.

join produce tutti i possibili candidati, tutti i record del set di dati A che sono meno recenti dei record del set di dati B e sono inclusi nel periodo di riferimento temporale. Quindi quello più vicino al set di dati B viene filtrato in base a arg_min (TimestampB - TimestampA). Quanto più corto è il periodo di riferimento temporale, tanto più accurati saranno i risultati della query.

Nell'esempio seguente il periodo di riferimento temporale è impostato su 1m. Il record con ID z non ha un evento A corrispondente perché il relativo evento A è più vecchio di due minuti.

let _maxLookbackPeriod = 1m;  
let _internalWindowBin = _maxLookbackPeriod / 2;
let B_events = B 
    | extend ID = new_guid()
    | extend _time = bin(Timestamp, _internalWindowBin)
    | extend _range = range(_time - _internalWindowBin, _time + _maxLookbackPeriod, _internalWindowBin) 
    | mv-expand _range to typeof(datetime) 
    | extend B_Timestamp = Timestamp, _range;
let A_events = A 
    | extend _time = bin(Timestamp, _internalWindowBin)
    | extend _range = range(_time - _internalWindowBin, _time + _maxLookbackPeriod, _internalWindowBin) 
    | mv-expand _range to typeof(datetime) 
    | extend A_Timestamp = Timestamp, _range;
B_events
    | join kind=leftouter (
        A_events
) on ID, _range
| where isnull(A_Timestamp) or (A_Timestamp <= B_Timestamp and B_Timestamp <= A_Timestamp + _maxLookbackPeriod)
| extend diff = coalesce(B_Timestamp - A_Timestamp, _maxLookbackPeriod*2)
| summarize arg_min(diff, *) by ID
| project ID, B_Timestamp, A_Timestamp, EventB, EventA
ID B_Timestamp A_Timestamp EventB EventA
x 2019-01-01 00:00:03.0000000 2019-01-01 00:00:01.0000000 B Ax2
x 2019-01-01 00:00:04.0000000 2019-01-01 00:00:01.0000000 B Ax2
y 2019-01-01 00:00:04.0000000 2019-01-01 00:00:02.0000000 B Ay1
z 2019-01-01 00:02:00.0000000 B

Passaggi successivi

Questo articolo identifica le esigenze di query comuni in Monitoraggio di Azure e illustra come usare il linguaggio di query Kusto per soddisfarle.

Operazioni con stringhe

Le sezioni seguenti contengono esempi su come usare le stringhe con il linguaggio di query Kusto.

Informazioni sulle stringhe e su come impostarle come escape

I valori di stringa sono racchiusi tra virgolette singole o doppie. Aggiungere la barra rovesciata (\) a sinistra di un carattere per eseguire l'escape del carattere: \t per tabulazione, \n per la nuova riga, \' per un carattere di virgoletta singola e \" per un carattere di virgoletta doppia.

print "this is a 'string' literal in double \" quotes"
print 'this is a "string" literal in single \' quotes'

Per impedire che "\" funzioni come carattere di escape, aggiungere "@" come prefisso alla stringa:

print @"C:\backslash\not\escaped\with @ prefix"

Confronti tra stringhe

Operatore Descrizione Con distinzione tra maiuscole e minuscole Esempio (restituisce true)
== Uguale a "aBc" == "aBc"
!= Diverso da "abc" != "ABC"
=~ Uguale a No "abc" =~ "ABC"
!~ Diverso da No "aBc" !~ "xyz"
has Il valore a destra è un termine intero nel valore a sinistra No "North America" has "america"
!has Il valore a destra non è un termine completo nel valore a sinistra No "North America" !has "amer"
has_cs Il valore a destra è un termine intero nel valore a sinistra "North America" has_cs "America"
!has_cs Il valore a destra non è un termine completo nel valore a sinistra "North America" !has_cs "amer"
hasprefix Il valore a destra è un prefisso di termine nel valore a sinistra No "North America" hasprefix "ame"
!hasprefix Il valore a destra non è un prefisso di termine nel valore a sinistra No "North America" !hasprefix "mer"
hasprefix_cs Il valore a destra è un prefisso di termine nel valore a sinistra "North America" hasprefix_cs "Ame"
!hasprefix_cs Il valore a destra non è un prefisso di termine nel valore a sinistra "North America" !hasprefix_cs "CA"
hassuffix Il valore a destra è un suffisso di termine nel valore a sinistra No "North America" hassuffix "ica"
!hassuffix Il valore a destra non è un suffisso di termine nel valore a sinistra No "North America" !hassuffix "americ"
hassuffix_cs Il valore a destra è un suffisso di termine nel valore a sinistra "North America" hassuffix_cs "ica"
!hassuffix_cs Il valore a destra non è un suffisso di termine nel valore a sinistra "North America" !hassuffix_cs "icA"
contains Il valore a destra ricorre come sottosequenza del valore a sinistra No "FabriKam" contains "BRik"
!contains Il valore a destra non ricorre nel valore a sinistra No "Fabrikam" !contains "xyz"
contains_cs Il valore a destra ricorre come sottosequenza del valore a sinistra "FabriKam" contains_cs "Kam"
!contains_cs Il valore a destra non ricorre nel valore a sinistra "Fabrikam" !contains_cs "Kam"
startswith Il valore a destra è una sottosequenza iniziale del valore a sinistra No "Fabrikam" startswith "fab"
!startswith Il valore a destra non è una sottosequenza iniziale del valore a sinistra No "Fabrikam" !startswith "kam"
startswith_cs Il valore a destra è una sottosequenza iniziale del valore a sinistra "Fabrikam" startswith_cs "Fab"
!startswith_cs Il valore a destra non è una sottosequenza iniziale del valore a sinistra "Fabrikam" !startswith_cs "fab"
endswith Il valore a destra è una sottosequenza di chiusura del valore a sinistra No "Fabrikam" endswith "Kam"
!endswith Il valore a destra non è una sottosequenza di chiusura del valore a sinistra No "Fabrikam" !endswith "brik"
endswith_cs Il valore a destra è una sottosequenza di chiusura del valore a sinistra "Fabrikam" endswith "Kam"
!endswith_cs Il valore a destra non è una sottosequenza di chiusura del valore a sinistra "Fabrikam" !endswith "brik"
matches regex Il valore a sinistra contiene una corrispondenza per il valore a destra "Fabrikam" matches regex "b.*k"
in È uguale a uno degli elementi "abc" in ("123", "345", "abc")
!in Non è uguale a nessuno degli elementi "bca" !in ("123", "345", "abc")

countof

Conta le occorrenze di una sottostringa all'interno di una stringa. Può corrispondere a stringhe di testo normale o usare un'espressione regolare (regex). Le corrispondenze con stringhe di testo normale possono sovrapporsi, mentre quelle con espressioni regolari no.

countof(text, search [, kind])
  • text: Stringa di input
  • search: stringa di testo normale o espressione regolare di cui trovare la corrispondenza nel testo
  • kind: normal | regex (valore predefinito: normal).

Restituisce il numero di volte in cui la stringa di ricerca può essere trovata nel contenitore. Le corrispondenze con stringhe di testo normale possono sovrapporsi, mentre quelle con espressioni regolari no.

Corrispondenze con stringhe di testo normale

print countof("The cat sat on the mat", "at");  //result: 3
print countof("aaa", "a");  //result: 3
print countof("aaaa", "aa");  //result: 3 (not 2!)
print countof("ababa", "ab", "normal");  //result: 2
print countof("ababa", "aba");  //result: 2

Corrispondenze con espressioni regolari

print countof("The cat sat on the mat", @"\b.at\b", "regex");  //result: 3
print countof("ababa", "aba", "regex");  //result: 1
print countof("abcabc", "a.c", "regex");  // result: 2

extract

Ottiene una corrispondenza per un'espressione regolare da una stringa specifica. Facoltativamente, può convertire la sottostringa estratta nel tipo specificato.

extract(regex, captureGroup, text [, typeLiteral])
  • regex: un'espressione regolare.
  • captureGroup: una costante intera positiva che indica il gruppo Capture da estrarre. Usare 0 per l'intera corrispondenza, 1 per il valore corrispondente alla prima parentesi () nell'espressione regolare e 2 o più per le parentesi successive.
  • text: la stringa in cui eseguire la ricerca.
  • typeLiteral: un valore letterale di tipo facoltativo, ad esempio typeof(long). Se specificato, la sottostringa estratta viene convertita nel tipo.

Restituisce la sottostringa corrispondente rispetto al gruppo Capture indicato captureGroup, facoltativamente convertita in typeLiteral. Se non ci sono corrispondenze o la conversione del tipo non riesce, restituisce Null.

L'esempio seguente estrae l'ultimo ottetto di ComputerIP da un record di heartbeat:

Heartbeat
| where ComputerIP != "" 
| take 1
| project ComputerIP, last_octet=extract("([0-9]*$)", 1, ComputerIP) 

L'esempio seguente estrae l'ultimo ottetto, ne esegue il cast a un tipo real (numero) e calcola il valore IP successivo:

Heartbeat
| where ComputerIP != "" 
| take 1
| extend last_octet=extract("([0-9]*$)", 1, ComputerIP, typeof(real)) 
| extend next_ip=(last_octet+1)%255
| project ComputerIP, last_octet, next_ip

Nell'esempio seguente viene cercata la definizione di Duration nella stringa Trace. Viene eseguito il cast della corrispondenza a un tipo real e il valore viene moltiplicato per una costante temporale (1 s) eseguendo il cast di Duration a un tipo timespan.

let Trace="A=12, B=34, Duration=567, ...";
print Duration = extract("Duration=([0-9.]+)", 1, Trace, typeof(real));  //result: 567
print Duration_seconds =  extract("Duration=([0-9.]+)", 1, Trace, typeof(real)) * time(1s);  //result: 00:09:27

isempty, isnotempty, notempty

  • isempty restituisce true se l'argomento è una stringa vuota o Null (vedere isnull).
  • isnotempty restituisce true se l'argomento non è una stringa vuota o Null (vedere isnotnull). Alias: notempty.
isempty(value)
isnotempty(value)

Esempio

print isempty("");  // result: true

print isempty("0");  // result: false

print isempty(0);  // result: false

print isempty(5);  // result: false

Heartbeat | where isnotempty(ComputerIP) | take 1  // return 1 Heartbeat record in which ComputerIP isn't empty

parseurl

Divide un URL in parti, come protocollo, host e porta, e quindi restituisce un oggetto dizionario contenente le parti come stringhe.

parseurl(urlstring)

Esempio

print parseurl("http://user:pass@contoso.com/icecream/buy.aspx?a=1&b=2#tag")

Di seguito è riportato l'output generato:

{
	"Scheme" : "http",
	"Host" : "contoso.com",
	"Port" : "80",
	"Path" : "/icecream/buy.aspx",
	"Username" : "user",
	"Password" : "pass",
	"Query Parameters" : {"a":"1","b":"2"},
	"Fragment" : "tag"
}

replace

Sostituire tutte le corrispondenze di espressioni regolari con un'altra stringa.

replace(regex, rewrite, input_text)
  • regex: l'espressione regolare in base a cui trovare una corrispondenza. Può contenere gruppi di acquisizione tra parentesi ().
  • rewrite: l'espressione regolare che sostituisce un'eventuale corrispondenza effettuata in base a un'espressione regolare. Usare \0 per fare riferimento all'intera corrispondenza, \1 per il primo gruppo Capture, \2 e così via per i gruppi Capture successivi.
  • input_text: la stringa di input in cui cercare.

Restituisce il testo dopo la sostituzione di tutte le corrispondenze dell'espressione regolare con le valutazioni di rewrite. Le corrispondenze non si sovrappongono.

Esempio

SecurityEvent
| take 1
| project Activity 
| extend replaced = replace(@"(\d+) -", @"Activity ID \1: ", Activity) 

Di seguito è riportato l'output generato:

Attività Sostituito
4663 - È stato effettuato un tentativo di accedere a un oggetto Activity ID 4663: È stato effettuato un tentativo di accedere a un oggetto.

split

Divide una determinata stringa in base a un delimitatore specificato e quindi restituisce una matrice di sottostringhe risultanti.

split(source, delimiter [, requestedIndex])
  • source: la stringa da dividere in base al delimitatore specificato.
  • delimiter: il delimitatore che verrà usato per dividere la stringa di origine.
  • requestedIndex: un indice in base zero facoltativo. Se specificato, la matrice di stringhe restituita contiene solo tale elemento, se esistente.

Esempio

print split("aaa_bbb_ccc", "_");    // result: ["aaa","bbb","ccc"]
print split("aa_bb", "_");          // result: ["aa","bb"]
print split("aaa_bbb_ccc", "_", 1);	// result: ["bbb"]
print split("", "_");              	// result: [""]
print split("a__b", "_");           // result: ["a","","b"]
print split("aabbcc", "bb");        // result: ["aa","cc"]

strcat

Concatena gli argomenti di stringa (supporta da 1 a 16 argomenti).

strcat("string1", "string2", "string3")

Esempio

print strcat("hello", " ", "world")	// result: "hello world"

strlen

Restituisce la lunghezza di una stringa.

strlen("text_to_evaluate")

Esempio

print strlen("hello")	// result: 5

substring

Estrarre una sottostringa da una determinata stringa di origine, a partire dall'indice specificato. Facoltativamente, è possibile specificare la lunghezza della sottostringa richiesta.

substring(source, startingIndex [, length])
  • source: la stringa di origine da cui viene estratta la sottostringa.
  • startingIndex: la posizione del carattere iniziale in base zero della sottostringa richiesta.
  • length: un parametro facoltativo che può essere usato per specificare la lunghezza richiesta della sottostringa restituita.

Esempio

print substring("abcdefg", 1, 2);	// result: "bc"
print substring("123456", 1);		// result: "23456"
print substring("123456", 2, 2);	// result: "34"
print substring("ABCD", 0, 2);	// result: "AB"

tolower, toupper

Converte una determinata stringa tutta in minuscolo o tutta in maiuscolo.

tolower("value")
toupper("value")

Esempio

print tolower("HELLO");	// result: "hello"
print toupper("hello");	// result: "HELLO"

Operazioni con data e ora

Le sezioni seguenti contengono esempi su come usare i valori di data e ora con il linguaggio di query Kusto.

Informazioni di base su data e ora

Nel linguaggio di query Kusto sono presenti due tipi di dati principali associati ai valori di data e ora: datetime e timespan. Tutte le date sono espresse in UTC. Benché siano supportati più formati di valori data e ora, è preferibile usare il formato ISO-8601.

I valori timespan sono espressi come numero decimale seguito da un'unità di tempo:

Sintassi abbreviata Unità di tempo
d day
h hour
m minute
s second
ms millisecondo
microsecondo microsecondo
tick nanosecondo

È possibile creare valori di data e ora eseguendo il cast di una stringa con l'operatore todatetime. Ad esempio, per esaminare gli heartbeat di una macchina virtuale inviati in un intervallo di tempo specifico, usare l'operatore between per specificare un intervallo di tempo:

Heartbeat
| where TimeGenerated between(datetime("2018-06-30 22:46:42") .. datetime("2018-07-01 00:57:27"))

Un altro scenario comune consiste nel confrontare un valore di data e ora con il presente. Ad esempio, per visualizzare tutti gli heartbeat negli ultimi due minuti, è possibile usare l'operatore now insieme a un valore timespan che rappresenta due minuti:

Heartbeat
| where TimeGenerated > now() - 2m

Per questa funzione è disponibile anche una soluzione più rapida:

Heartbeat
| where TimeGenerated > now(-2m)

Il metodo più rapido e leggibile consiste nell'usare l'operatore ago:

Heartbeat
| where TimeGenerated > ago(2m)

Si supponga di conoscere l'ora di inizio e la durata anziché l'ora di inizio e di fine. È possibile riscrivere la query:

let startDatetime = todatetime("2018-06-30 20:12:42.9");
let duration = totimespan(25m);
Heartbeat
| where TimeGenerated between(startDatetime .. (startDatetime+duration) )
| extend timeFromStart = TimeGenerated - startDatetime

Convertire le unità di tempo

Può essere utile esprimere un valore di data e ora o timespan in un'unità di tempo diversa da quella predefinita. Se ad esempio si esaminano gli eventi di errore degli ultimi 30 minuti ed è necessario aggiungere una colonna calcolata che mostra quanto tempo prima si è verificato l'evento, è possibile usare questa query:

Event
| where TimeGenerated > ago(30m)
| where EventLevelName == "Error"
| extend timeAgo = now() - TimeGenerated 

La colonna timeAgo contiene valori come 00:09:31.5118992, in formato hh:mm:ss.fffffff. Per formattare questi valori con il number di minuti dall'ora di inizio, dividere tale valore per 1m:

Event
| where TimeGenerated > ago(30m)
| where EventLevelName == "Error"
| extend timeAgo = now() - TimeGenerated
| extend timeAgoMinutes = timeAgo/1m 

Aggregazioni e bucket in base all'intervallo di tempo

Un altro scenario comune è rappresentato dalla necessità di ottenere statistiche relative a un determinato periodo di tempo con una specifica unità di tempo. Per questo scenario, è possibile usare un operatore bin come parte di una clausola summarize.

Usare la query seguente per ottenere il numero di eventi che si sono verificati ogni cinque minuti durante l'ultima mezz'ora:

Event
| where TimeGenerated > ago(30m)
| summarize events_count=count() by bin(TimeGenerated, 5m) 

Questa query restituisce la tabella seguente:

TimeGenerated(UTC) events_count
2018-08-01T09:30:00 54
2018-08-01T09:35:00 41
2018-08-01T09:40:00 42
2018-08-01T09:45:00 41
2018-08-01T09:50:00 41
2018-08-01T09:55:00 16

Un altro modo per creare bucket di risultati consiste nell'usare funzioni come startofday:

Event
| where TimeGenerated > ago(4d)
| summarize events_count=count() by startofday(TimeGenerated) 

Di seguito è riportato l'output generato:

timestamp count_
2018-07-28T00:00:00 7.136
2018-07-29T00:00:00 12.315
2018-07-30T00:00:00 16.847
2018-07-31T00:00:00 12.616
2018-08-01T00:00:00 5.416

Fusi orari

Dato che tutti i valori di data e ora sono espressi in UTC, spesso è utile convertirli nel fuso orario locale. Eseguire ad esempio questo calcolo per convertire le ore UTC in PST:

Event
| extend localTimestamp = TimeGenerated - 8h

Aggregations

Le sezioni seguenti contengono esempi su come aggregare i risultati di una query con il linguaggio di query Kusto.

count

Questa funzione conta il numero di righe nel set di risultati dopo l'applicazione di eventuali filtri. L'esempio seguente restituisce il numero totale di righe della tabella Perf negli ultimi 30 minuti. I risultati vengono restituiti in una colonna denominata count_ a meno che non si definisca un nome specifico:

Perf
| where TimeGenerated > ago(30m) 
| summarize count()
Perf
| where TimeGenerated > ago(30m) 
| summarize num_of_records=count() 

Per visualizzare una tendenza nel tempo può essere utile un diagramma temporale:

Perf 
| where TimeGenerated > ago(30m) 
| summarize count() by bin(TimeGenerated, 5m)
| render timechart

L'output di questo esempio mostra la linea di tendenza dei conteggi dei record Perf a intervalli di cinque minuti:

Screenshot of a line chart that shows the Perf record count trend line in five-minute intervals.

dcount, dcountif

Usare dcount e dcountif per contare valori distinti in una colonna specifica. La query seguente valuta quanti computer distinti hanno inviato heartbeat nell'ultima ora:

Heartbeat 
| where TimeGenerated > ago(1h) 
| summarize dcount(Computer)

Per contare solo i computer Linux che hanno inviato heartbeat, usare dcountif:

Heartbeat 
| where TimeGenerated > ago(1h) 
| summarize dcountif(Computer, OSType=="Linux")

Valutare i sottogruppi

Per eseguire un conteggio o altre aggregazioni in sottogruppi dei dati, usare la parola chiave by. Ad esempio, per contare il numero di computer Linux distinti che hanno inviato heartbeat in ogni paese o area geografica, usare questa query:

Heartbeat 
| where TimeGenerated > ago(1h) 
| summarize distinct_computers=dcountif(Computer, OSType=="Linux") by RemoteIPCountry
RemoteIPCountry distinct_computers
Stati Uniti 19
Canada 3
Irlanda 0
Regno Unito 0
Paesi Bassi 2

Per analizzare sottogruppi ancora più piccoli dei dati, aggiungere nomi di colonna nella sezione by. Può ad esempio essere necessario contare i computer distinti di ogni paese o area geografica in base al tipo di sistema operativo (OSType):

Heartbeat 
| where TimeGenerated > ago(1h) 
| summarize distinct_computers=dcountif(Computer, OSType=="Linux") by RemoteIPCountry, OSType

Percentile

Per trovare il valore mediano, usare la funzione percentile con un valore per specificare il percentile:

Perf
| where TimeGenerated > ago(30m) 
| where CounterName == "% Processor Time" and InstanceName == "_Total" 
| summarize percentiles(CounterValue, 50) by Computer

È anche possibile specificare diversi percentili per ottenere un risultato aggregato per ciascuno:

Perf
| where TimeGenerated > ago(30m) 
| where CounterName == "% Processor Time" and InstanceName == "_Total" 
| summarize percentiles(CounterValue, 25, 50, 75, 90) by Computer

I risultati potrebbero mostrare che alcune CPU di computer hanno valori mediani simili. Tuttavia, mentre alcuni computer sono stabili intorno alla mediana, altri riportano valori di CPU molto più bassi e più alti. I valori alti e bassi significano che nei computer si sono verificati picchi.

Variance

Per valutare direttamente la varianza di un valore, usare i metodi di varianza e deviazione standard:

Perf
| where TimeGenerated > ago(30m) 
| where CounterName == "% Processor Time" and InstanceName == "_Total" 
| summarize stdev(CounterValue), variance(CounterValue) by Computer

Un metodo valido per analizzare la stabilità dell'utilizzo della CPU consiste nel combinare stdev con il calcolo della mediana:

Perf
| where TimeGenerated > ago(130m) 
| where CounterName == "% Processor Time" and InstanceName == "_Total" 
| summarize stdev(CounterValue), percentiles(CounterValue, 50) by Computer

Generare elenchi e set

È possibile usare makelist per variare la visualizzazione dei dati in base all'ordine dei valori di una determinata colonna. Se ad esempio è necessario esaminare l'ordine più comune in cui si verificano gli eventi nei computer, è possibile scegliere di visualizzare i dati in base all'ordine dei valori di EventID in ogni computer:

Event
| where TimeGenerated > ago(12h)
| order by TimeGenerated desc
| summarize makelist(EventID) by Computer

Di seguito è riportato l'output generato:

Computer list_EventID
computer1 [704,701,1501,1500,1085,704,704,701]
computer2 [326,105,302,301,300,102]
... ...

makelist genera un elenco nell'ordine in cui sono stati passati i dati. Per ordinare gli eventi dal meno recente al più recente, usare asc nell'istruzione order invece di desc.

Può risultare utile creare un elenco contenente solo valori distinti. Questo elenco è detto set ed è possibile generarlo con il comando makeset:

Event
| where TimeGenerated > ago(12h)
| order by TimeGenerated desc
| summarize makeset(EventID) by Computer

Di seguito è riportato l'output generato:

Computer list_EventID
computer1 [704,701,1501,1500,1085]
computer2 [326,105,302,301,300,102]
... ...

Analogamente a makelist, anche makeset funziona con i dati ordinati. Il comando makeset genera matrici basate sull'ordine delle righe passate.

Espandere gli elenchi

L'operazione inversa di makelist o makeset è mv-expand. Il comando mv-expand espande un elenco di valori in righe separate. Può espanderli in un numero qualsiasi di colonne dinamiche, sia JSON che di matrice. È ad esempio possibile controllare la tabella Heartbeat per cercare le soluzioni che hanno trasmesso dati dai computer che hanno inviato un heartbeat nell'ultima ora:

Heartbeat
| where TimeGenerated > ago(1h)
| project Computer, Solutions

Di seguito è riportato l'output generato:

Computer Soluzioni
computer1 "security", "updates", "changeTracking"
computer2 "security", "updates"
computer3 "antiMalware", "changeTracking"
... ...

Usare mv-expand per mostrare ogni valore in una riga separata invece che in un elenco di valori delimitati da virgole:

Heartbeat
| where TimeGenerated > ago(1h)
| project Computer, split(Solutions, ",")
| mv-expand Solutions

Di seguito è riportato l'output generato:

Computer Soluzioni
computer1 "security"
computer1 "updates"
computer1 "changeTracking"
computer2 "security"
computer2 "updates"
computer3 "antiMalware"
computer3 "changeTracking"
... ...

È possibile usare makelist per raggruppare gli elementi. Nell'output è possibile vedere l'elenco di computer per ogni soluzione:

Heartbeat
| where TimeGenerated > ago(1h)
| project Computer, split(Solutions, ",")
| mv-expand Solutions
| summarize makelist(Computer) by tostring(Solutions) 

Di seguito è riportato l'output generato:

Soluzioni list_Computer
"security" ["computer1", "computer2"]
"updates" ["computer1", "computer2"]
"changeTracking" ["computer1", "computer3"]
"antiMalware" ["computer3"]
... ...

Bin mancanti

Un'applicazione utile di mv-expand è quella di inserire i valori predefiniti per i bin mancanti. Si supponga, ad esempio, di voler conoscere il tempo di attività di un determinato computer esaminandone l'heartbeat e di voler visualizzare l'origine dell'heartbeat che è riportata nella colonna Category. Normalmente si userebbe un'istruzione summarize di base:

Heartbeat
| where TimeGenerated > ago(12h)
| summarize count() by Category, bin(TimeGenerated, 1h)

Di seguito è riportato l'output generato:

Category TimeGenerated count_
Agente diretto 2017-06-06T17:00:00Z 15
Agente diretto 2017-06-06T18:00:00Z 60
Agente diretto 2017-06-06T20:00:00Z 55
Agente diretto 2017-06-06T21:00:00Z 57
Agente diretto 2017-06-06T22:00:00Z 60
... ... ...

Nell'output l'intervallo di tempo associato a "2017-06-06T19:00:00Z" risulta mancante perché non sono presenti dati relativi a heartbeat per tale ora. Per assegnare un valore predefinito agli intervalli di tempo vuoti usare la funzione make-series. Per ogni categoria viene generata una riga. L'output include due colonne di matrice supplementari, una per i valori e l'altra per gli intervalli di tempo corrispondenti:

Heartbeat
| make-series count() default=0 on TimeGenerated in range(ago(1d), now(), 1h) by Category 

Di seguito è riportato l'output generato:

Category count_ TimeGenerated
Agente diretto [15,60,0,55,60,57,60,...] ["2017-06-06T17:00:00.0000000Z","2017-06-06T18:00:00.0000000Z","2017-06-06T19:00:00.0000000Z","2017-06-06T20:00:00.0000000Z","2017-06-06T21:00:00.0000000Z",...]
... ... ...

Il terzo elemento della matrice count_ è 0, come previsto. La matrice TimeGenerated ha un timestamp corrispondente, "2017-06-06T19:00:00.0000000Z". Ma il formato di questa matrice è difficile da leggere. Usare mv-expand per espandere le matrici e generare l'output nello stesso formato di quello generato da summarize:

Heartbeat
| make-series count() default=0 on TimeGenerated in range(ago(1d), now(), 1h) by Category 
| mv-expand TimeGenerated, count_
| project Category, TimeGenerated, count_

Di seguito è riportato l'output generato:

Category TimeGenerated count_
Agente diretto 2017-06-06T17:00:00Z 15
Agente diretto 2017-06-06T18:00:00Z 60
Agente diretto 2017-06-06T19:00:00Z 0
Agente diretto 2017-06-06T20:00:00Z 55
Agente diretto 2017-06-06T21:00:00Z 57
Agente diretto 2017-06-06T22:00:00Z 60
... ... ...

Restringere i risultati a un set di elementi: let, makeset, toscalar, in

Uno scenario comune consiste nel selezionare i nomi di alcune entità specifiche in base a un set di criteri e quindi filtrare un altro set di dati fino a tale set di entità. Può ad esempio essere necessario trovare i computer a cui non risultano applicati aggiornamenti e identificare gli indirizzi IP a cui questi computer hanno inviato chiamate.

Ecco un esempio:

let ComputersNeedingUpdate = toscalar(
    Update
    | summarize makeset(Computer)
    | project set_Computer
);
WindowsFirewall
| where Computer in (ComputersNeedingUpdate)

Join

È possibile usare i join per analizzare nella stessa query i dati provenienti da più tabelle. Un join unisce le righe di due set di dati associando i valori di determinate colonne.

Ecco un esempio:

SecurityEvent 
| where EventID == 4624		// sign-in events
| project Computer, Account, TargetLogonId, LogonTime=TimeGenerated
| join kind= inner (
    SecurityEvent 
    | where EventID == 4634		// sign-out events
    | project TargetLogonId, LogoffTime=TimeGenerated
) on TargetLogonId
| extend Duration = LogoffTime-LogonTime
| project-away TargetLogonId1 
| top 10 by Duration desc

In questo esempio il primo set di dati filtra tutti gli eventi di accesso, e viene unito tramite join a un secondo set di dati che filtra tutti gli eventi di disconnessione. Le colonne proiettate sono Computer, Account, TargetLogonId e TimeGenerated. I set di dati sono correlati in base a una colonna condivisa, TargetLogonId. L'output è costituito da un singolo record per correlazione, che include sia l'ora di accesso sia l'ora di disconnessione.

Se i due set di dati includono colonne con lo stesso nome, a quelle del set di dati a destra viene assegnato un numero di indice. In questo esempio i risultati mostrano TargetLogonId con i valori della tabella di sinistra e TargetLogonId1 con quelli della tabella di destra. In questo caso la seconda colonna TargetLogonId1 è stata rimossa tramite l'operatore project-away.

Nota

Per migliorare le prestazioni, mantenere solo le colonne pertinenti dei set di dati uniti tramite join usando l'operatore project.

Usare la sintassi seguente per creare un join tra due set di dati in cui la chiave di join ha un nome diverso tra le due tabelle:

Table1
| join ( Table2 ) 
on $left.key1 == $right.key2

Tabelle di ricerca

Un uso comune dei join consiste nell'usare datatable per il mapping di valori. L'uso di datatable può aiutare a rendere i risultati più presentabili. Ad esempio, è possibile arricchire i dati degli eventi di sicurezza con il nome dell'evento per ogni ID evento:

let DimTable = datatable(EventID:int, eventName:string)
  [
    4625, "Account activity",
    4688, "Process action",
    4634, "Account activity",
    4658, "The handle to an object was closed",
    4656, "A handle to an object was requested",
    4690, "An attempt was made to duplicate a handle to an object",
    4663, "An attempt was made to access an object",
    5061, "Cryptographic operation",
    5058, "Key file operation"
  ];
SecurityEvent
| join kind = inner
 DimTable on EventID
| summarize count() by eventName

Di seguito è riportato l'output generato:

eventName count_
L'handle per un oggetto è stato chiuso 290.995
È stato richiesto un handle per un oggetto 154.157
È stato effettuato un tentativo di duplicare un handle per un oggetto 144.305
È stato effettuato un tentativo di accedere a un oggetto 123.669
Operazione crittografica 153.495
Operazione sul file di chiave 153.496

Strutture dei dati e JSON

Gli oggetti annidati sono oggetti che ne contengono altri in una matrice o in una mappa di coppie chiave-valore. Questi oggetti sono rappresentati come stringhe JSON. Questa sezione descrive come usare JSON per recuperare i dati e analizzare gli oggetti annidati.

Usare le stringe JSON

Usare extractjson per accedere a un elemento JSON specifico in un percorso noto. Questa funzione richiede un'espressione di percorso che usa le convenzioni seguenti:

  • Usare $ per fare riferimento alla cartella radice.
  • Usare la notazione tra parentesi quadre o con punto per fare riferimento agli indici e agli elementi, come illustrato negli esempi seguenti.

Usare le parentesi quadre per gli indici e i punti per separare gli elementi:

let hosts_report='{"hosts": [{"location":"North_DC", "status":"running", "rate":5},{"location":"South_DC", "status":"stopped", "rate":3}]}';
print hosts_report
| extend status = extractjson("$.hosts[0].status", hosts_report)

Questo esempio è simile, ma usa solo la notazione con parentesi quadre:

let hosts_report='{"hosts": [{"location":"North_DC", "status":"running", "rate":5},{"location":"South_DC", "status":"stopped", "rate":3}]}';
print hosts_report 
| extend status = extractjson("$['hosts'][0]['status']", hosts_report)

Per un solo elemento, è possibile usare solo la notazione con punto:

let hosts_report=dynamic({"location":"North_DC", "status":"running", "rate":5});
print hosts_report 
| extend status = hosts_report.status

parsejson

È più facile accedere a più elementi in una struttura JSON come oggetto dinamico. Usare parsejson per eseguire il cast dei dati di testo in un oggetto dinamico. Dopo aver convertito il codice JSON in un tipo dinamico, è possibile usare altre funzioni per analizzare i dati.

let hosts_object = parsejson('{"hosts": [{"location":"North_DC", "status":"running", "rate":5},{"location":"South_DC", "status":"stopped", "rate":3}]}');
print hosts_object 
| extend status0=hosts_object.hosts[0].status, rate1=hosts_object.hosts[1].rate

arraylength

Usare arraylength per contare il numero di elementi in una matrice:

let hosts_object = parsejson('{"hosts": [{"location":"North_DC", "status":"running", "rate":5},{"location":"South_DC", "status":"stopped", "rate":3}]}');
print hosts_object 
| extend hosts_num=arraylength(hosts_object.hosts)

mv-expand

Usare mv-expand per dividere le proprietà di un oggetto in righe separate:

let hosts_object = parsejson('{"hosts": [{"location":"North_DC", "status":"running", "rate":5},{"location":"South_DC", "status":"stopped", "rate":3}]}');
print hosts_object 
| mv-expand hosts_object.hosts[0]

Screenshot shows hosts_0 with values for location, status, and rate.

buildschema

Usare buildschema per ottenere lo schema che ammette tutti i valori di un oggetto:

let hosts_object = parsejson('{"hosts": [{"location":"North_DC", "status":"running", "rate":5},{"location":"South_DC", "status":"stopped", "rate":3}]}');
print hosts_object 
| summarize buildschema(hosts_object)

Il risultato è uno schema in formato JSON:

{
    "hosts":
    {
        "indexer":
        {
            "location": "string",
            "rate": "int",
            "status": "string"
        }
    }
}

Lo schema descrive i nomi dei campi dell'oggetto e i tipi di dati corrispondenti.

Gli oggetti annidati possono avere schemi diversi, come nell'esempio seguente:

let hosts_object = parsejson('{"hosts": [{"location":"North_DC", "status":"running", "rate":5},{"status":"stopped", "rate":"3", "range":100}]}');
print hosts_object 
| summarize buildschema(hosts_object)

Grafici

Le sezioni seguenti contengono esempi su come usare i grafici con il linguaggio di query Kusto.

Tracciare i risultati su grafico

Per iniziare, esaminare il numero di computer disponibili nell'ultima ora per ogni sistema operativo:

Heartbeat
| where TimeGenerated > ago(1h)
| summarize count(Computer) by OSType  

Per impostazione predefinita, i risultati vengono visualizzati sotto forma di tabella:

Screenshot that shows query results in a table.

Per una visualizzazione più utile dei risultati, selezionare Grafico e scegliere l'opzione Torta:

Screenshot that shows query results in a pie chart.

Grafici di tempo

Visualizzare la media di tempo processore al 50° e al 95° percentile in bin di un'ora.

La query seguente genera più serie. Nei risultati è possibile scegliere quale serie mostrare nel diagramma temporale.

Perf
| where TimeGenerated > ago(1d) 
| where CounterName == "% Processor Time" 
| summarize avg(CounterValue), percentiles(CounterValue, 50, 95)  by bin(TimeGenerated, 1h)

Selezionare l'opzione di visualizzazione corrispondente al grafico a linee:

Screenshot that shows a multiple-series line chart.

Linea di riferimento

Una linea di riferimento consente di identificare facilmente se la metrica supera una soglia specifica. Per aggiungere una linea a un grafico, estendere il set di dati con una colonna costante:

Perf
| where TimeGenerated > ago(1d) 
| where CounterName == "% Processor Time" 
| summarize avg(CounterValue), percentiles(CounterValue, 50, 95)  by bin(TimeGenerated, 1h)
| extend Threshold = 20

Screenshot that shows a multiple-series line chart with a threshold reference line.

Uso di più dimensioni

La presenza di più espressioni nella clausola by di summarize determina la creazione di più righe nei risultati. Viene creata una riga per ogni combinazione di valori.

SecurityEvent
| where TimeGenerated > ago(1d)
| summarize count() by tostring(EventID), AccountType, bin(TimeGenerated, 1h)

Quando si visualizzano i risultati in un grafico, viene usata la prima colonna della clausola by. L'esempio seguente illustra un istogramma in pila creato usando il valore EventID. Le dimensioni devono essere di tipo string. In questo esempio, viene eseguito il cast del valore EventID a string:

Screenshot that shows a bar chart based on the EventID column.

È possibile passare a un'altra colonna selezionandola nella freccia a discesa con i nomi di colonna:

Screenshot that shows a bar chart based on AccountType column, with the column selector visible.

Funzioni di analisi avanzate

Questa sezione contiene esempi che usano le funzioni di analisi intelligente di Azure Log Analytics per analizzare l'attività utente. È possibile usare questi esempi per analizzare le applicazioni monitorate da Azure Application Insights o usare i concetti di queste query per eseguire analisi simili su altri dati.

Analisi di coorte

Con l'analisi di coorte viene monitorata l'attività di gruppi specifici di utenti, definiti coorti. Lo scopo è quello di provare a misurare quanto risulta accattivante un servizio calcolando la percentuale di utenti che tornano a usarlo. Gli utenti vengono raggruppati in base alla data in cui usano il servizio per la prima volta. Quando si analizzano le coorti, viene previsto un calo di attività nei primi periodi monitorati. Il titolo di ogni coorte si riferisce alla settimana in cui i membri del gruppo sono stati osservati per la prima volta.

L'esempio seguente analizza il numero di attività completate dagli utenti durante le cinque settimane successive al primo utilizzo del servizio:

let startDate = startofweek(bin(datetime(2017-01-20T00:00:00Z), 1d));
let week = range Cohort from startDate to datetime(2017-03-01T00:00:00Z) step 7d;
// For each user, we find the first and last timestamp of activity
let FirstAndLastUserActivity = (end:datetime) 
{
    customEvents
    | where customDimensions["sourceapp"]=="ai-loganalyticsui-prod"
    // Check 30 days back to see first time activity.
    | where timestamp > startDate - 30d
    | where timestamp < end      
    | summarize min=min(timestamp), max=max(timestamp) by user_AuthenticatedId
};
let DistinctUsers = (cohortPeriod:datetime, evaluatePeriod:datetime) {
    toscalar (
    FirstAndLastUserActivity(evaluatePeriod)
    // Find members of the cohort: only users that were observed in this period for the first time.
    | where min >= cohortPeriod and min < cohortPeriod + 7d  
    // Pick only the members that were active during the evaluated period or after.
    | where max > evaluatePeriod - 7d
    | summarize dcount(user_AuthenticatedId)) 
};
week 
| where Cohort == startDate
// Finally, calculate the desired metric for each cohort. In this sample, we calculate distinct users but you can change
// this to any other metric that would measure the engagement of the cohort members.
| extend 
    r0 = DistinctUsers(startDate, startDate+7d),
    r1 = DistinctUsers(startDate, startDate+14d),
    r2 = DistinctUsers(startDate, startDate+21d),
    r3 = DistinctUsers(startDate, startDate+28d),
    r4 = DistinctUsers(startDate, startDate+35d)
| union (week | where Cohort == startDate + 7d 
| extend 
    r0 = DistinctUsers(startDate+7d, startDate+14d),
    r1 = DistinctUsers(startDate+7d, startDate+21d),
    r2 = DistinctUsers(startDate+7d, startDate+28d),
    r3 = DistinctUsers(startDate+7d, startDate+35d) )
| union (week | where Cohort == startDate + 14d 
| extend 
    r0 = DistinctUsers(startDate+14d, startDate+21d),
    r1 = DistinctUsers(startDate+14d, startDate+28d),
    r2 = DistinctUsers(startDate+14d, startDate+35d) )
| union (week | where Cohort == startDate + 21d 
| extend 
    r0 = DistinctUsers(startDate+21d, startDate+28d),
    r1 = DistinctUsers(startDate+21d, startDate+35d) ) 
| union (week | where Cohort == startDate + 28d 
| extend 
    r0 = DistinctUsers (startDate+28d, startDate+35d) )
// Calculate the retention percentage for each cohort by weeks
| project Cohort, r0, r1, r2, r3, r4,
          p0 = r0/r0*100,
          p1 = todouble(r1)/todouble (r0)*100,
          p2 = todouble(r2)/todouble(r0)*100,
          p3 = todouble(r3)/todouble(r0)*100,
          p4 = todouble(r4)/todouble(r0)*100 
| sort by Cohort asc

Di seguito è riportato l'output generato:

Screenshot that shows a table of cohorts based on activity.

Persistenza degli utenti e utenti attivi mensili in sequenza

L'esempio seguente usa l'analisi delle serie temporali con la funzione series_fir. È possibile usare la funzione series_fir per i calcoli con finestre temporali scorrevoli. L'applicazione di esempio monitorata è un negozio online che tiene traccia dell'attività degli utenti attraverso eventi personalizzati. La query tiene traccia di due tipi di attività utente: AddToCart e Checkout. Definisce attivo un utente che ha completato una transazione almeno una volta in uno specifico giorno.

let endtime = endofday(datetime(2017-03-01T00:00:00Z));
let window = 60d;
let starttime = endtime-window;
let interval = 1d;
let user_bins_to_analyze = 28;
// Create an array of filters coefficients for series_fir(). A list of '1' in our case will produce a simple sum.
let moving_sum_filter = toscalar(range x from 1 to user_bins_to_analyze step 1 | extend v=1 | summarize makelist(v)); 
// Level of engagement. Users will be counted as engaged if they completed at least this number of activities.
let min_activity = 1;
customEvents
| where timestamp > starttime  
| where customDimensions["sourceapp"] == "ai-loganalyticsui-prod"
// We want to analyze users who actually checked out in our website.
| where (name == "Checkout") and user_AuthenticatedId <> ""
// Create a series of activities per user.
| make-series UserClicks=count() default=0 on timestamp 
	in range(starttime, endtime-1s, interval) by user_AuthenticatedId
// Create a new column that contains a sliding sum. 
// Passing 'false' as the last parameter to series_fir() prevents normalization of the calculation by the size of the window.
// For each time bin in the *RollingUserClicks* column, the value is the aggregation of the user activities in the 
// 28 days that preceded the bin. For example, if a user was active once on 2016-12-31 and then inactive throughout 
// January, then the value will be 1 between 2016-12-31 -> 2017-01-28 and then 0s. 
| extend RollingUserClicks=series_fir(UserClicks, moving_sum_filter, false)
// Use the zip() operator to pack the timestamp with the user activities per time bin.
| project User_AuthenticatedId=user_AuthenticatedId , RollingUserClicksByDay=zip(timestamp, RollingUserClicks)
// Transpose the table and create a separate row for each combination of user and time bin (1 day).
| mv-expand RollingUserClicksByDay
| extend Timestamp=todatetime(RollingUserClicksByDay[0])
// Mark the users that qualify according to min_activity.
| extend RollingActiveUsersByDay=iff(toint(RollingUserClicksByDay[1]) >= min_activity, 1, 0)
// And finally, count the number of users per time bin.
| summarize sum(RollingActiveUsersByDay) by Timestamp
// First 28 days contain partial data, so we filter them out.
| where Timestamp > starttime + 28d
// Render as timechart.
| render timechart

Di seguito è riportato l'output generato:

Screenshot of a chart that shows rolling active users by day over a month.

L'esempio seguente converte la query precedente in una funzione riutilizzabile. Usa quindi la query per calcolare la persistenza degli utenti in sequenza. In questa query viene definito attivo un utente che ha completato una transazione almeno una volta in uno specifico giorno.

let rollingDcount = (sliding_window_size: int, event_name:string)
{
    let endtime = endofday(datetime(2017-03-01T00:00:00Z));
    let window = 90d;
    let starttime = endtime-window;
    let interval = 1d;
    let moving_sum_filter = toscalar(range x from 1 to sliding_window_size step 1 | extend v=1| summarize makelist(v));    
    let min_activity = 1;
    customEvents
    | where timestamp > starttime
    | where customDimensions["sourceapp"]=="ai-loganalyticsui-prod"
    | where (name == event_name)
    | where user_AuthenticatedId <> ""
    | make-series UserClicks=count() default=0 on timestamp 
		in range(starttime, endtime-1s, interval) by user_AuthenticatedId
    | extend RollingUserClicks=fir(UserClicks, moving_sum_filter, false)
    | project User_AuthenticatedId=user_AuthenticatedId , RollingUserClicksByDay=zip(timestamp, RollingUserClicks)
    | mv-expand RollingUserClicksByDay
    | extend Timestamp=todatetime(RollingUserClicksByDay[0])
    | extend RollingActiveUsersByDay=iff(toint(RollingUserClicksByDay[1]) >= min_activity, 1, 0)
    | summarize sum(RollingActiveUsersByDay) by Timestamp
    | where Timestamp > starttime + 28d
};
// Use the moving_sum_filter with bin size of 28 to count MAU.
rollingDcount(28, "Checkout")
| join
(
    // Use the moving_sum_filter with bin size of 1 to count DAU.
    rollingDcount(1, "Checkout")
)
on Timestamp
| project sum_RollingActiveUsersByDay1 *1.0 / sum_RollingActiveUsersByDay, Timestamp
| render timechart

Di seguito è riportato l'output generato:

Screenshot of a chart that shows user stickiness over time.

Analisi di regressione

Questo esempio illustra come creare uno strumento di rilevamento automatico delle interruzioni del servizio basato esclusivamente sui log di traccia di un'applicazione. Lo strumento di rilevamento cerca incrementi anomali e improvvisi della quantità relativa di tracce di errori e avvisi nell'applicazione.

Per valutare lo stato del servizio in base ai dati dei log di traccia, vengono usate due tecniche:

  • Usare make-series per convertire i log di traccia testuali semistrutturati in una metrica che rappresenti il rapporto tra righe di traccia positive e negative.
  • Usare series_fit_2lines e series_fit_line per eseguire un rilevamento avanzato "step-jump" tramite l'analisi delle serie temporali con regressione lineare a due righe.
let startDate = startofday(datetime("2017-02-01"));
let endDate = startofday(datetime("2017-02-07"));
let minRsquare = 0.8;  // Tune the sensitivity of the detection sensor. Values close to 1 indicate very low sensitivity.
// Count all Good (Verbose + Info) and Bad (Error + Fatal + Warning) traces, per day.
traces
| where timestamp > startDate and timestamp < endDate
| summarize 
    Verbose = countif(severityLevel == 0),
    Info = countif(severityLevel == 1), 
    Warning = countif(severityLevel == 2),
    Error = countif(severityLevel == 3),
    Fatal = countif(severityLevel == 4) by bin(timestamp, 1d)
| extend Bad = (Error + Fatal + Warning), Good = (Verbose + Info)
// Determine the ratio of bad traces, from the total.
| extend Ratio = (todouble(Bad) / todouble(Good + Bad))*10000
| project timestamp , Ratio
// Create a time series.
| make-series RatioSeries=any(Ratio) default=0 on timestamp in range(startDate , endDate -1d, 1d) by 'TraceSeverity' 
// Apply a 2-line regression to the time series.
| extend (RSquare2, SplitIdx, Variance2,RVariance2,LineFit2)=series_fit_2lines(RatioSeries)
// Find out if our 2-line is trending up or down.
| extend (Slope,Interception,RSquare,Variance,RVariance,LineFit)=series_fit_line(LineFit2)
// Check whether the line fit reaches the threshold, and if the spike represents an increase (rather than a decrease).
| project PatternMatch = iff(RSquare2 > minRsquare and Slope>0, "Spike detected", "No Match")

Passaggi successivi