Fundamentação Teórica: Query and Analyze Logs in Azure Monitor
1. Intuição Inicial​
Imagine que sua empresa mantém um arquivo central com registros de tudo que acontece: quem entrou no prédio, quais sistemas foram acessados, quais erros ocorreram, quais mudanças foram feitas. Esse arquivo cresce terabytes por dia. Para extrair valor dele, você precisa de uma linguagem de consulta que permita perguntar coisas como: "Mostre todos os acessos negados das últimas 6 horas, agrupados por usuário, ordenados pelo que tentou mais vezes."
Kusto Query Language (KQL) é essa linguagem de consulta para os logs armazenados no Azure Monitor Log Analytics. É uma linguagem projetada especificamente para consultar grandes volumes de dados de série temporal de forma rápida e expressiva.
A beleza do KQL é que ele é lido como uma cadeia de transformações: você pega uma tabela de dados, filtra o que não interessa, seleciona os campos relevantes, agrupa, calcula e apresenta o resultado. É intuitivo uma vez que você entende o padrão tabela | operador | operador | operador.
2. Contexto​
2.1 KQL dentro do Azure Monitor​
2.2 Principais tabelas que você precisa conhecer​
Antes de escrever queries, é preciso saber onde os dados estão. As tabelas mais importantes no AZ-104:
| Tabela | Conteúdo |
|---|---|
AzureActivity | Activity Log: criações, modificações, exclusões de recursos |
AzureDiagnostics | Logs de diagnóstico de múltiplos recursos (legacy) |
StorageBlobLogs | Operações de Blob Storage |
AzureMetrics | Métricas exportadas para Log Analytics |
SecurityEvent | Eventos de segurança do Windows (Event Log) |
Syslog | Logs do sistema Linux |
Heartbeat | Pulsação periódica de VMs monitoradas pelo agente |
SigninLogs | Tentativas de login no Azure AD |
AuditLogs | Ações administrativas no Azure AD |
Event | Eventos do Windows Event Log genérico |
Perf | Contadores de performance de VMs (CPU, memória, disco) |
3. Construção dos Conceitos​
3.1 O modelo mental do KQL: pipeline de dados​
O conceito central do KQL é o operador pipe (|): cada operador recebe uma tabela como entrada, aplica uma transformação e passa o resultado para o próximo operador.
tabela | operador1 | operador2 | operador3
É exatamente como o pipe do Unix/Linux: cat file.log | grep ERROR | sort | uniq -c
3.2 Operadores fundamentais​
where: Filtra linhas com base em uma condição. É o operador mais usado.
AzureActivity
| where TimeGenerated > ago(24h)
| where ActivityStatus == "Failed"
project: Seleciona apenas as colunas que você quer (como SELECT em SQL).
AzureActivity
| where TimeGenerated > ago(24h)
| project TimeGenerated, Caller, OperationName, ActivityStatus
extend: Adiciona uma nova coluna calculada sem remover as existentes.
Perf
| where CounterName == "% Processor Time"
| extend CPUCategory = iff(CounterValue > 80, "High", "Normal")
summarize: Agrega dados, como GROUP BY no SQL. Sempre usado com funções de agregação.
AzureActivity
| where TimeGenerated > ago(7d)
| summarize OperationCount = count() by Caller, ActivityStatus
| order by OperationCount desc
order by / sort by: Ordena os resultados.
SecurityEvent
| where EventID == 4625 // Failed logon
| summarize FailedAttempts = count() by Account
| order by FailedAttempts desc
| take 10
take / limit: Retorna apenas os N primeiros resultados. Útil para exploração rápida.
distinct: Retorna valores únicos de uma coluna.
AzureActivity
| distinct Caller
count: Conta o número total de registros.
AzureActivity
| where TimeGenerated > ago(24h)
| count
3.3 Funções de tempo: essenciais para logs​
ago(): Retorna um timestamp no passado relativo ao momento atual.
| where TimeGenerated > ago(1h) // última hora
| where TimeGenerated > ago(7d) // últimos 7 dias
| where TimeGenerated > ago(30m) // últimos 30 minutos
bin(): Agrupa timestamps em intervalos regulares. Essencial para criar séries temporais.
AzureActivity
| where TimeGenerated > ago(24h)
| summarize EventCount = count() by bin(TimeGenerated, 1h)
| order by TimeGenerated asc
startofday(), startofweek(), startofmonth(): Retorna o inÃcio do perÃodo relativo a um timestamp.
3.4 Funções de string​
contains: Verifica se uma string contém um trecho (case-insensitive por padrão).
| where OperationName contains "delete"
startswith, endswith: Verificam prefixo ou sufixo.
tolower(), toupper(): Normalizam capitalização.
split(): Divide uma string em array pelo separador.
strcat(): Concatena strings.
extract(): Extrai um padrão via regex.
| extend IPAddress = extract(@"(\d+\.\d+\.\d+\.\d+)", 1, CallerIpAddress)
3.5 Funções de agregação​
| Função | Descrição | Exemplo |
|---|---|---|
count() | Conta registros | summarize Total = count() |
sum(campo) | Soma valores | summarize TotalBytes = sum(ResponseBodySize) |
avg(campo) | Média | summarize AvgLatency = avg(DurationMs) |
max(campo) | Valor máximo | summarize PeakCPU = max(CounterValue) |
min(campo) | Valor mÃnimo | summarize MinMemory = min(Available_MBytes) |
percentile(campo, N) | Percentil N | summarize P95 = percentile(DurationMs, 95) |
dcount(campo) | Contagem distinta | summarize UniqueUsers = dcount(Caller) |
makeset(campo) | Cria um array de valores distintos | summarize ResourceList = makeset(Resource) |
3.6 join: unindo tabelas​
O join combina duas tabelas com base em um campo em comum. Similar ao JOIN do SQL mas com tipos especÃficos:
// Cruzar erros de aplicação com eventos de segurança no mesmo perÃodo
let errorTimes = AppExceptions
| where TimeGenerated > ago(1h)
| project ErrorTime = TimeGenerated, ProblemId;
SecurityEvent
| where TimeGenerated > ago(1h)
| join kind=inner errorTimes on $left.TimeGenerated == $right.ErrorTime
Tipos de join mais usados:
inner: Apenas registros com match em ambos os ladosleftouter: Todos da esquerda, com match da direita quando existeanti: Registros da esquerda que NÃO têm match na direita
3.7 let: variáveis e subqueries​
O let define variáveis ou subqueries nomeadas para reutilização.
let threshold = 80;
let highCPUVMs = Perf
| where CounterName == "% Processor Time"
| where CounterValue > threshold
| distinct Computer;
Heartbeat
| where Computer in (highCPUVMs)
| summarize LastHeartbeat = max(TimeGenerated) by Computer
3.8 union: combinando resultados de múltiplas tabelas​
// Ver todos os erros de Storage e SQL no mesmo resultado
union StorageBlobLogs, AzureDiagnostics
| where TimeGenerated > ago(1h)
| where Level == "Error"
| project TimeGenerated, ResourceType, OperationName, Level
| order by TimeGenerated desc
4. Visão Estrutural​
5. Funcionamento na Prática​
5.1 Queries essenciais para o AZ-104​
Quem deletou recursos nas últimas 24 horas?
AzureActivity
| where TimeGenerated > ago(24h)
| where OperationName contains "delete"
| where ActivityStatus == "Succeeded"
| project TimeGenerated, Caller, OperationName, ResourceGroup, Resource
| order by TimeGenerated desc
Quais VMs estão sem heartbeat (potencialmente offline)?
Heartbeat
| where TimeGenerated > ago(5m)
| summarize LastHeartbeat = max(TimeGenerated) by Computer
| where LastHeartbeat < ago(5m)
| order by LastHeartbeat asc
Top 10 IPs com falhas de login no Windows:
SecurityEvent
| where EventID == 4625
| where TimeGenerated > ago(24h)
| summarize FailedAttempts = count() by IpAddress
| top 10 by FailedAttempts
Uso de CPU de VMs nas últimas 4 horas (série temporal):
Perf
| where TimeGenerated > ago(4h)
| where CounterName == "% Processor Time"
| where InstanceName == "_Total"
| summarize AvgCPU = avg(CounterValue) by Computer, bin(TimeGenerated, 15m)
| render timechart
Erros no Storage Account nas últimas 6 horas:
StorageBlobLogs
| where TimeGenerated > ago(6h)
| where StatusCode >= 400
| summarize ErrorCount = count() by StatusCode, OperationName
| order by ErrorCount desc
Mudanças no Azure (Activity Log) por tipo de operação:
AzureActivity
| where TimeGenerated > ago(7d)
| where ActivityStatus == "Succeeded"
| summarize OperationCount = count() by OperationName
| top 20 by OperationCount
| render barchart
Quais usuários fizeram mais ações na assinatura:
AzureActivity
| where TimeGenerated > ago(30d)
| summarize ActionCount = count() by Caller
| where Caller !contains "microsoft" // excluir serviços Microsoft internos
| top 10 by ActionCount
5.2 O operador render: visualização inline​
O render transforma o resultado da query em um gráfico diretamente no Log Analytics Explorer:
// Série temporal de eventos por hora
AzureActivity
| where TimeGenerated > ago(24h)
| summarize Count = count() by bin(TimeGenerated, 1h)
| render timechart
// Gráfico de barras por tipo de operação
AzureActivity
| summarize Count = count() by Category
| render barchart
// Gráfico de pizza de distribuição
AzureActivity
| summarize Count = count() by ActivityStatus
| render piechart
Tipos de render: timechart, barchart, columnchart, piechart, scatterchart, table.
5.3 Comportamentos não óbvios​
Case-sensitivity: KQL é case-sensitive por padrão. "Error" não é igual a "error". Use =~ para comparação case-insensitive:
| where ActivityStatus =~ "succeeded" // insensÃvel a maiúsculas
| where ActivityStatus == "Succeeded" // sensÃvel a maiúsculas
Timestamps: O campo TimeGenerated é sempre em UTC. Se você está em UTC-3, um evento das 14h local aparece como 17h no KQL.
Tipo de dados dinâmicos: Alguns campos são do tipo dynamic (JSON embutido). Para acessar propriedades:
| extend statusCode = tostring(Properties.statusCode)
| extend vmName = tostring(Properties.entity.name)
6. Formas de Implementação​
6.1 Log Analytics Workspace (Portal Azure)​
Quando usar: Investigação interativa, troubleshooting, exploração de dados, criação de visualizações, salvar queries para reutilização.
Acesso: Azure Monitor > Logs ou Log Analytics Workspace > Logs
O portal oferece:
- IntelliSense para autocompletar tabelas e campos
- Histórico de queries executadas
- Salvar queries em "Saved Queries" para compartilhar com a equipe
- Pin para dashboard
- Export de resultados para CSV ou JSON
6.2 Azure CLI​
# Executar query KQL via CLI
az monitor log-analytics query \
--workspace <workspace-id> \
--analytics-query "AzureActivity | where TimeGenerated > ago(1h) | take 10" \
--output table
# Query com parâmetros de tempo
az monitor log-analytics query \
--workspace <workspace-id> \
--analytics-query "AzureActivity | where ActivityStatus == 'Failed' | count" \
--start-time 2025-01-15T00:00:00Z \
--end-time 2025-01-15T23:59:59Z
Quando usar: Scripts de automação, relatórios periódicos, integração com pipelines CI/CD.
6.3 Azure PowerShell​
# Executar query KQL
$query = @"
AzureActivity
| where TimeGenerated > ago(24h)
| where ActivityStatus == 'Failed'
| project TimeGenerated, Caller, OperationName
| order by TimeGenerated desc
"@
Invoke-AzOperationalInsightsQuery `
-WorkspaceId <workspace-id> `
-Query $query |
Select-Object -ExpandProperty Results |
Format-Table
6.4 REST API (Azure Monitor Query API)​
Para integração com aplicações e sistemas externos:
curl -X POST \
"https://api.loganalytics.io/v1/workspaces/<workspace-id>/query" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"query": "AzureActivity | where TimeGenerated > ago(1h) | count",
"timespan": "PT1H"
}'
6.5 Azure Monitor Workbooks​
Workbooks são dashboards interativos que combinam text, queries KQL, métricas e visualizações. São ideais para relatórios repetÃveis de operações ou segurança.
Acesso: Azure Monitor > Workbooks > + New
Você pode adicionar múltiplas seções, cada uma com sua própria query KQL e tipo de visualização.
7. Controle e Segurança​
7.1 Controle de acesso a logs​
| Role | Acesso |
|---|---|
| Log Analytics Reader | Consultar logs, visualizar configurações |
| Log Analytics Contributor | Tudo do Reader + criar alertas e workbooks |
| Monitoring Reader | Consultar logs e métricas (somente leitura) |
| Owner/Contributor na assinatura | Acesso completo |
Table-level RBAC: Para dados sensÃveis, você pode restringir acesso por tabela. Um usuário pode ter acesso ao workspace mas não conseguir consultar SecurityEvent ou SigninLogs.
az monitor log-analytics workspace table update \
--resource-group myRG \
--workspace-name myLAW \
--name SecurityEvent \
--plan Analytics
7.2 Queries que expõem dados sensÃveis​
Algumas tabelas contêm PII (Personally Identifiable Information) ou dados sensÃveis:
SigninLogs: Endereços IP, usernames, localização dos usuáriosAuditLogs: Ações de usuários incluindo acesso a dadosSecurityEvent: Nomes de usuário e atividade de loginStorageBlobLogs: Nomes de arquivos acessados, IPs dos clientes
Restrinja acesso a essas tabelas a apenas os times que precisam.
8. Tomada de Decisão​
8.1 Qual operador usar para cada necessidade​
| Necessidade | Operador | Exemplo |
|---|---|---|
| Filtrar por condição | where | where Level == "Error" |
| Selecionar colunas | project | project Time, User, Action |
| Adicionar coluna calculada | extend | extend Severity = iff(Code>400,"High","Low") |
| Contar e agrupar | summarize + count() | summarize Count = count() by User |
| Ordenar resultados | order by | order by Count desc |
| Limitar resultados | take / top | top 10 by Count |
| Valores únicos | distinct | distinct User |
| Combinar tabelas | join | join kind=inner T2 on Field |
| Unir resultados | union | union T1, T2 |
| Criar variável | let | let threshold = 80; |
| Visualizar gráfico | render | render timechart |
8.2 Estratégia de investigação de incidentes​
| Fase | Query recomendada | Objetivo |
|---|---|---|
| Detecção | where Level == "Error" com count() | Quantificar o problema |
| Contexto temporal | bin(TimeGenerated, 5m) com render timechart | Ver quando começou |
| Identificar fonte | summarize count() by Computer/User/Resource | Descobrir origem |
| Detalhes | project com campos especÃficos + take 100 | Examinar eventos individuais |
| Correlação | join com outra tabela | Cruzar com outros eventos |
9. Boas Práticas​
- Sempre filtre por
TimeGeneratedprimeiro. Queries sem filtro de tempo varrem todo o histórico e são lentas e caras. Coloquewhere TimeGenerated > ago(Xh)como a primeira linha após a tabela. - Use
projectpara reduzir colunas antes desummarize. Menos colunas processadas = queries mais rápidas. - Use
top N by campoem vez deorder by | take N.topé otimizado para esse padrão. - Salve queries úteis em "Saved Queries" no portal para compartilhar com a equipe e reutilizar durante incidentes.
- Use
letpara subqueries complexas e para definir thresholds que podem ser ajustados facilmente no inÃcio da query. - Teste queries com
take 100primeiro para validar a lógica antes de executar aggregations em grandes volumes. - Use
render timechartpara qualquer análise temporal. Padrões ficam imediatamente visÃveis em gráficos que seriam invisÃveis numa tabela de números. - Documente queries de investigação em um repositório (GitHub, Azure DevOps) para construir um runbook de operações.
10. Erros Comuns​
| Erro | Por que acontece | Como evitar |
|---|---|---|
| Query lenta ou timeout | Sem filtro de TimeGenerated | Sempre iniciar com where TimeGenerated > ago(Xh) |
| Resultados incorretos por case | Comparação com == em campo com capitalização variável | Usar =~ para case-insensitive |
Campo dynamic não acessÃvel | Tentar comparar campo JSON diretamente | Usar tostring(Properties.campo) antes de comparar |
summarize removendo colunas não esperadas | summarize só mantém colunas listadas no by | Usar project para selecionar antes do summarize |
join retornando produto cartesiano | Muitos matches nos dois lados | Pré-filtrar tabelas antes do join ou usar kind=leftouter |
ago() retornando perÃodo errado | Confundir ago(7d) com "data X" | Usar datetime(2025-01-15) para datas absolutas |
| Resultados com UTC vs horário local | TimeGenerated em UTC | Usar datetime_local_to_utc() ou ajustar na apresentação |
| Query correta mas sem resultados | Dados ainda em trânsito (latência de ingestão) | Aguardar 5-10 minutos e repetir |
11. Operação e Manutenção​
11.1 Queries de saúde do próprio Log Analytics​
Volume de ingestão por tabela:
Usage
| where TimeGenerated > ago(7d)
| summarize TotalGB = round(sum(Quantity) / 1024, 2) by DataType
| order by TotalGB desc
Latência de ingestão:
AzureActivity
| where TimeGenerated > ago(1h)
| extend IngestDelay = ingestion_time() - TimeGenerated
| summarize AvgDelayMin = avg(IngestDelay / 1m)
Queries mais lentas executadas (requer diagnósticos do LAW habilitados):
LAQueryLogs
| where TimeGenerated > ago(1d)
| order by ResponseDurationMs desc
| take 20
| project TimeGenerated, RequestClientApp, ResponseDurationMs, QueryText
11.2 Limites importantes​
| Aspecto | Limite |
|---|---|
| Tempo máximo de execução de query | 10 minutos |
| Tamanho máximo do resultado | 64 MB |
| Número máximo de linhas no resultado | 500.000 |
| Janela de tempo máxima por query | Limitada pela retenção do workspace |
| Queries simultâneas por workspace | 200 |
join colunas no lado direito | Máximo de 1 milhão de registros |
12. Integração e Automação​
12.1 Alert Rules baseadas em KQL​
# Criar alerta baseado em query KQL
az monitor scheduled-query alert create \
--resource-group myRG \
--name "Failed-Login-Alert" \
--scopes <workspace-resource-id> \
--condition-query "SecurityEvent | where EventID == 4625 | where TimeGenerated > ago(5m) | summarize count() | where count_ > 10" \
--condition-threshold 0 \
--condition-operator "GreaterThan" \
--evaluation-frequency 5m \
--window-size 5m \
--severity 2 \
--action-groups <action-group-id>
12.2 Workbook automatizado para relatório diário​
Crie Workbooks com queries parametrizadas por intervalo de tempo. Configure para publicação automática ou envio por email via Logic Apps que executam queries KQL via API.
12.3 Exportando resultados de queries via Pipeline​
# Exportar resultado de query para CSV via CLI
az monitor log-analytics query \
--workspace <workspace-id> \
--analytics-query "AzureActivity | where TimeGenerated > ago(7d) | where ActivityStatus == 'Failed' | project TimeGenerated, Caller, OperationName" \
--output json | \
jq -r '.[] | [.TimeGenerated, .Caller, .OperationName] | @csv' > failed-operations.csv
13. Resumo Final​
Conceitos essenciais:
- KQL usa o padrão de pipeline:
tabela | operador | operador. Cada operador transforma a saÃda do anterior. - Os operadores mais importantes são:
where(filtrar),project(selecionar colunas),extend(adicionar colunas),summarize(agregar),order by(ordenar),take/top(limitar). - Sempre filtre por
TimeGeneratedcomo primeiro operador para performance e custo. rendertransforma resultados em gráficos inline (timechart,barchart,piechart).
Diferenças crÃticas:
==vs=~:==é case-sensitive;=~é case-insensitive.projectvsextend:projectseleciona apenas as colunas listadas (remove as demais).extendadiciona novas colunas sem remover as existentes.takevstop:take Nretorna os primeiros N sem ordenação especÃfica.top N by camporetorna os N maiores/menores por um campo (mais eficiente queorder by | take).count(operador tabular) vscount()(função de agregação):| countconta tudo e retorna uma linha.summarize count() by campoagrupa e conta por categoria.ago()vsdatetime():ago(7d)é relativo ao momento atual.datetime(2025-01-15)é absoluto.
O que precisa ser lembrado:
- O campo
TimeGeneratedestá sempre em UTC. - Campos do tipo
dynamic(JSON) requeremtostring(campo.subcampo)antes de comparar ou usar. - O operador
summarizeremove todas as colunas que não aparecem noby. Useprojectantes para selecionar o que precisa. - Queries sem filtro de tempo são lentas, caras e muitas vezes retornam timeout.
leté poderoso para criar subqueries nomeadas e definir constantes que facilitem ajustes na query.- A latência de ingestão de logs é de 2 a 5 minutos. Consultas para eventos muito recentes podem não retornar resultados ainda.
- Salve queries úteis em Saved Queries do workspace para reutilização durante incidentes, quando o tempo é crÃtico.