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
etop
per limitare la quantità di dati visualizzati. - Disporre i risultati per definire l'ordine dell'asse x.
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:
- Usare let per assegnare un nome a una proiezione della tabella semplificata il più possibile prima di avviare il join.
- 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. - Usare join per associare le voci di avvio e di arresto per la stessa attività. Per ogni attività viene creata una riga.
- 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 parametroStartTime
. 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.
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
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 funzionebin()
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:
- Usare l'operatore
union
per aggiungere altre righe a una tabella. Queste righe vengono prodotte dall'espressioneunion
. - L'operatore
range
produce una tabella con una singola riga e una singola colonna. La tabella viene usata esclusivamente per le operazioni dimv-expand
. - L'operatore
mv-expand
sulla funzionerange
crea il numero di righe corrispondente al numero di bin di cinque minuti compresi traStartTime
eEndTime
. - Usare un valore
0
perCount
. - L'operatore
summarize
raggruppa i bin dell'argomento originale (left, ovvero outer) inunion
. 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, comeComponent
eCluster
. - 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
.
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"]
Creare una tabella del contenuto del dispositivo:
DeviceModel FriendlyName iPhone5,1 iPhone 5 iPhone3,2 iPhone 4 iPhone7,2 iPhone 6 iPhone5,2 iPhone5 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]
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):
datatable
è un modo per produrre alcuni dati di test a scopo dimostrativo. In genere, si usano dati reali.- Questa riga essenzialmente significa restituire tutti i valori distinti di
id
. - 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
- La colonna
- 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. - 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:
- Inserire ogni record i un bin di un giorno, rispetto a
_start
. - 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. - Per ogni record, creare una matrice di setti giorni (timestamp) a partire dal giorno del record corrente.
- Usare
mv-expand
sulla matrice, duplicando così ogni record in sette record, con un giorno di distanza uno dall'altro. - Eseguire la funzione di aggregazione per ogni giorno. A causa del passaggio 4, questo passaggio effettivamente riepiloga gli ultimi sette giorni.
- 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
- Seguire un'esercitazione sul linguaggio di query Kusto.
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 | Sì | "aBc" == "aBc" |
!= |
Diverso da | Sì | "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 | Sì | "North America" has_cs "America" |
!has_cs |
Il valore a destra non è un termine completo nel valore a sinistra | Sì | "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 | Sì | "North America" hasprefix_cs "Ame" |
!hasprefix_cs |
Il valore a destra non è un prefisso di termine nel valore a sinistra | Sì | "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 | Sì | "North America" hassuffix_cs "ica" |
!hassuffix_cs |
Il valore a destra non è un suffisso di termine nel valore a sinistra | Sì | "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 | Sì | "FabriKam" contains_cs "Kam" |
!contains_cs |
Il valore a destra non ricorre nel valore a sinistra | Sì | "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 | Sì | "Fabrikam" startswith_cs "Fab" |
!startswith_cs |
Il valore a destra non è una sottosequenza iniziale del valore a sinistra | Sì | "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 | Sì | "Fabrikam" endswith "Kam" |
!endswith_cs |
Il valore a destra non è una sottosequenza di chiusura del valore a sinistra | Sì | "Fabrikam" !endswith "brik" |
matches regex |
Il valore a sinistra contiene una corrispondenza per il valore a destra | Sì | "Fabrikam" matches regex "b.*k" |
in |
È uguale a uno degli elementi | Sì | "abc" in ("123", "345", "abc") |
!in |
Non è uguale a nessuno degli elementi | Sì | "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 inputsearch
: stringa di testo normale o espressione regolare di cui trovare la corrispondenza nel testokind
: 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 esempiotypeof(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
restituiscetrue
se l'argomento è una stringa vuota o Null (vedereisnull
).isnotempty
restituiscetrue
se l'argomento non è una stringa vuota o Null (vedereisnotnull
). 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:
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]
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:
Per una visualizzazione più utile dei risultati, selezionare Grafico e scegliere l'opzione Torta:
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:
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
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
:
È possibile passare a un'altra colonna selezionandola nella freccia a discesa con i nomi di colonna:
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:
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:
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:
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
- Seguire un'esercitazione sul linguaggio di query Kusto.