Pular para o conteúdo principal

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​

100%
Scroll para zoom · Arraste para mover · 📱 Pinch para zoom no celular

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:

TabelaConteúdo
AzureActivityActivity Log: criações, modificações, exclusões de recursos
AzureDiagnosticsLogs de diagnóstico de múltiplos recursos (legacy)
StorageBlobLogsOperações de Blob Storage
AzureMetricsMétricas exportadas para Log Analytics
SecurityEventEventos de segurança do Windows (Event Log)
SyslogLogs do sistema Linux
HeartbeatPulsação periódica de VMs monitoradas pelo agente
SigninLogsTentativas de login no Azure AD
AuditLogsAções administrativas no Azure AD
EventEventos do Windows Event Log genérico
PerfContadores 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çãoDescriçãoExemplo
count()Conta registrossummarize Total = count()
sum(campo)Soma valoressummarize TotalBytes = sum(ResponseBodySize)
avg(campo)Médiasummarize AvgLatency = avg(DurationMs)
max(campo)Valor máximosummarize PeakCPU = max(CounterValue)
min(campo)Valor mínimosummarize MinMemory = min(Available_MBytes)
percentile(campo, N)Percentil Nsummarize P95 = percentile(DurationMs, 95)
dcount(campo)Contagem distintasummarize UniqueUsers = dcount(Caller)
makeset(campo)Cria um array de valores distintossummarize 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 lados
  • leftouter: Todos da esquerda, com match da direita quando existe
  • anti: 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​

100%
Scroll para zoom · Arraste para mover · 📱 Pinch para zoom no celular

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​

RoleAcesso
Log Analytics ReaderConsultar logs, visualizar configurações
Log Analytics ContributorTudo do Reader + criar alertas e workbooks
Monitoring ReaderConsultar logs e métricas (somente leitura)
Owner/Contributor na assinaturaAcesso 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ários
  • AuditLogs: Ações de usuários incluindo acesso a dados
  • SecurityEvent: Nomes de usuário e atividade de login
  • StorageBlobLogs: 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​

NecessidadeOperadorExemplo
Filtrar por condiçãowherewhere Level == "Error"
Selecionar colunasprojectproject Time, User, Action
Adicionar coluna calculadaextendextend Severity = iff(Code>400,"High","Low")
Contar e agruparsummarize + count()summarize Count = count() by User
Ordenar resultadosorder byorder by Count desc
Limitar resultadostake / toptop 10 by Count
Valores únicosdistinctdistinct User
Combinar tabelasjoinjoin kind=inner T2 on Field
Unir resultadosunionunion T1, T2
Criar variávelletlet threshold = 80;
Visualizar gráficorenderrender timechart

8.2 Estratégia de investigação de incidentes​

FaseQuery recomendadaObjetivo
Detecçãowhere Level == "Error" com count()Quantificar o problema
Contexto temporalbin(TimeGenerated, 5m) com render timechartVer quando começou
Identificar fontesummarize count() by Computer/User/ResourceDescobrir origem
Detalhesproject com campos específicos + take 100Examinar eventos individuais
Correlaçãojoin com outra tabelaCruzar com outros eventos

9. Boas Práticas​

  • Sempre filtre por TimeGenerated primeiro. Queries sem filtro de tempo varrem todo o histórico e são lentas e caras. Coloque where TimeGenerated > ago(Xh) como a primeira linha após a tabela.
  • Use project para reduzir colunas antes de summarize. Menos colunas processadas = queries mais rápidas.
  • Use top N by campo em vez de order 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 let para subqueries complexas e para definir thresholds que podem ser ajustados facilmente no início da query.
  • Teste queries com take 100 primeiro para validar a lógica antes de executar aggregations em grandes volumes.
  • Use render timechart para 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​

ErroPor que aconteceComo evitar
Query lenta ou timeoutSem filtro de TimeGeneratedSempre iniciar com where TimeGenerated > ago(Xh)
Resultados incorretos por caseComparação com == em campo com capitalização variávelUsar =~ para case-insensitive
Campo dynamic não acessívelTentar comparar campo JSON diretamenteUsar tostring(Properties.campo) antes de comparar
summarize removendo colunas não esperadassummarize só mantém colunas listadas no byUsar project para selecionar antes do summarize
join retornando produto cartesianoMuitos matches nos dois ladosPré-filtrar tabelas antes do join ou usar kind=leftouter
ago() retornando período erradoConfundir ago(7d) com "data X"Usar datetime(2025-01-15) para datas absolutas
Resultados com UTC vs horário localTimeGenerated em UTCUsar datetime_local_to_utc() ou ajustar na apresentação
Query correta mas sem resultadosDados 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​

AspectoLimite
Tempo máximo de execução de query10 minutos
Tamanho máximo do resultado64 MB
Número máximo de linhas no resultado500.000
Janela de tempo máxima por queryLimitada pela retenção do workspace
Queries simultâneas por workspace200
join colunas no lado direitoMá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 TimeGenerated como primeiro operador para performance e custo.
  • render transforma resultados em gráficos inline (timechart, barchart, piechart).

Diferenças críticas:

  • == vs =~: == é case-sensitive; =~ é case-insensitive.
  • project vs extend: project seleciona apenas as colunas listadas (remove as demais). extend adiciona novas colunas sem remover as existentes.
  • take vs top: take N retorna os primeiros N sem ordenação específica. top N by campo retorna os N maiores/menores por um campo (mais eficiente que order by | take).
  • count (operador tabular) vs count() (função de agregação): | count conta tudo e retorna uma linha. summarize count() by campo agrupa e conta por categoria.
  • ago() vs datetime(): ago(7d) é relativo ao momento atual. datetime(2025-01-15) é absoluto.

O que precisa ser lembrado:

  • O campo TimeGenerated está sempre em UTC.
  • Campos do tipo dynamic (JSON) requerem tostring(campo.subcampo) antes de comparar ou usar.
  • O operador summarize remove todas as colunas que não aparecem no by. Use project antes 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.