En la lección anterior configuraste las configuraciones de diagnóstico y todo empezó a fluir hacia log-contoso-pro. Ahora hay datos: peticiones HTTP de la web de reservas, consultas lentas de db-reservas, accesos a kv-contoso-pro, registros de los contenedores de aks-contoso-operaciones y el registro de actividad de las dos suscripciones. Almacenarlos no sirve de nada si nadie sabe interrogarlos, y ese es exactamente el punto donde muchos equipos se quedan: pagan la ingesta y no explotan el dato.

Esta lección enseña KQL —el lenguaje de consulta de Kusto— desde cero y de forma progresiva, siempre sobre datos reales de Contoso Airlines. Terminarás resolviendo con él la queja que abrió el módulo: «he pagado y no me ha llegado la tarjeta de embarque». Y aprenderás a controlar la factura, porque la ingesta y la retención en Log Analytics son la partida que más sorprende cuando llega el mes siguiente.

Contenido

  1. Qué es un área de trabajo de Log Analytics
  2. Diseño del área: una o varias, y RBAC
  3. Las tablas que te vas a encontrar
  4. KQL desde cero: filtrar, proyectar y extender
  5. Agregar: summarize, bin() y series temporales
  6. Combinar y transformar: join, union, let, parse y mv-expand
  7. La investigación completa de una tarjeta que no llegó
  8. Funciones guardadas y alertas de búsqueda de registros
  9. Consultas entre áreas y exportación de datos
  10. Planes de tabla, retención y las tres palancas del coste
  11. Errores Comunes y Consejos
  12. Ejercicios
  13. Conclusión

  1. Qué es un área de trabajo de Log Analytics

Un área de trabajo es el contenedor donde se guardan y consultan los registros: un recurso de Azure, en una región concreta, con su RBAC, su retención y su factura. Dentro hay tablas, cada una con un esquema fijo de columnas tipadas, y sobre ellas se consulta con KQL, un lenguaje de solo lectura, de estilo tubería, pensado para volúmenes enormes de datos de series temporales.

log-contoso-pro vive en rg-contoso-seguridad-pro y es deliberadamente el punto único de convergencia. Ahí llegan los registros de recurso de toda la plataforma, la telemetría de Application Insights (07-03), los datos de Container Insights de AKS, el registro de actividad y las alertas de Microsoft Defender for Cloud del módulo 4. Esa convergencia es lo que permite escribir una sola consulta que cruza la petición HTTP del pasajero con la excepción de la función y con el mensaje de la cola. Si esos datos estuvieran en cuatro áreas distintas, la investigación sería un ejercicio de copiar y pegar entre pestañas.

  1. Diseño del área: una o varias, y RBAC

La primera decisión de arquitectura es cuántas áreas crear:

Motivo para separar Motivo para unificar
Residencia legal del dato en otra región, o aislamiento exigido por un cliente Correlacionar entre servicios en una sola consulta
Costes que deben facturarse a suscripciones distintas Un solo conjunto de retención y planes que mantener
Retenciones legalmente incompatibles Alertas y libros que abarcan toda la plataforma

Contoso usa una sola área de producción. Antes de decidirlo, el argumento en contra era el control de acceso: no todo el mundo debe ver los registros de kv-contoso-pro. La respuesta está en los dos modos de acceso:

  • Contexto de área: quien tiene permiso sobre el área ve todas las tablas y todos los recursos; es lo que necesita el equipo central de operaciones.
  • Contexto de recurso: el usuario consulta desde la hoja del recurso, o con resource(), y ve solo los registros de los recursos sobre los que tiene RBAC. Un desarrollador con Lector sobre app-contoso-reservas-dev ve esos y ninguno más, aunque estén en la misma área.

Se activa con az monitor log-analytics workspace update --workspace-name log-contoso-pro --resource-group rg-contoso-seguridad-pro --set properties.features.enableLogAccessUsingOnlyResourcePermissions=true. Con esa opción, el permiso sobre el recurso gobierna y el del área deja de dar acceso universal: el grupo Contoso-Desarrollo recibe Lector sobre rg-contoso-reservas-dev y ve lo suyo; Contoso-Operaciones recibe el rol Lector de Log Analytics sobre el área y lo ve todo. Una sola área, permisos por recurso: exactamente el RBAC del módulo 4 aplicado a la observabilidad.

  1. Las tablas que te vas a encontrar

Tabla Qué contiene Origen
AzureActivity Operaciones del plano de control: quién creó, cambió o borró Registro de actividad
AzureDiagnostics / AppServiceHTTPLogs Registros de recurso en esquema genérico o en tabla dedicada Configuración de diagnóstico
AppRequests Peticiones vistas por la aplicación, con duración y resultado Application Insights
AppTraces / AppExceptions Mensajes de registro del código y excepciones con su pila Application Insights
AppDependencies Llamadas salientes: SQL, HTTP, colas Application Insights
ContainerLogV2 Salida estándar de los contenedores de AKS Container Insights
Heartbeat / SecurityAlert Latido de cada agente y alertas de Defender for Cloud Agente / Defender
Usage Cuánto ha ingerido cada tabla: la tabla del dinero Interna, gratuita

Dos advertencias. AzureDiagnostics es una tabla ancha y compartida por muchos servicios, con columnas del tipo campo_s, campo_d o campo_b y un límite de columnas que puede provocar pérdidas: prefiere siempre las tablas dedicadas con --export-to-resource-specific true, como hiciste en 07-01. Y los nombres App* son los de un recurso de Application Insights basado en área de trabajo: la telemetría vive en log-contoso-pro y se consulta junto al resto, que es justo lo que hace posible el apartado 7.

  1. KQL desde cero: filtrar, proyectar y extender

Una consulta KQL empieza por una tabla y encadena operadores con la barra vertical |. Cada operador recibe una tabla y devuelve otra.

AppServiceHTTPLogs
| where TimeGenerated > ago(1h)
| where CsHost == "reservas.contosoairlines.example" and ScStatus >= 500
| project TimeGenerated, CsUriStem, ScStatus, TimeTaken, CIp
| order by TimeGenerated desc
| take 20

Línea a línea:

  1. AppServiceHTTPLogs — la tabla de origen. Empezar siempre por una tabla concreta, nunca por search.
  2. where TimeGenerated > ago(1h) — filtro temporal; admite 5m, 7d, 30d. Va el primero siempre: es el que reduce el volumen escaneado y, por tanto, el coste y el tiempo de la consulta.
  3. where sucesivos filtran por sitio y por código de estado: == distingue mayúsculas, =~ no.
  4. project selecciona columnas y descarta el resto, reduciendo lo que viaja al navegador; order by ... desc ordena —sin él el orden no está garantizado— y take 20 corta el resultado.

El operador search "XR7742" busca un texto en todas las tablas: útil una vez, cuando no sabes dónde está el dato, y carísimo porque lo escanea todo. En cuanto sepas la tabla, úsala directamente. Por su parte, extend añade columnas calculadas sin perder las existentes:

AppServiceHTTPLogs
| where TimeGenerated between (datetime(2026-08-14 08:00) .. datetime(2026-08-14 12:00))
| extend SegundosRespuesta = TimeTaken / 1000.0
| extend Familia = case(ScStatus < 400, "ok", ScStatus < 500, "error de cliente", "error de servidor")
| where SegundosRespuesta > 2
| project TimeGenerated, CsUriStem, SegundosRespuesta, Familia
  • between (datetime(...) .. datetime(...)) acota una ventana absoluta, para reconstruir un incidente ya cerrado. Otros operadores de tiempo frecuentes: startofday(now()), startofweek(), now()-2d.
  • extend crea SegundosRespuesta a partir de milisegundos; el .0 fuerza la división decimal.
  • case(...) evalúa condiciones en orden y devuelve el primer acierto; el último valor es el «si no».

  1. Agregar: summarize, bin() y series temporales

summarize es el operador que convierte millones de filas en una respuesta.

AppRequests
| where TimeGenerated > ago(24h) and AppRoleName == "app-contoso-api-disponibilidad-pro"
| summarize Peticiones = count(), Fallidas = countif(Success == false), MediaMs = avg(DurationMs),
            P95Ms = percentile(DurationMs, 95), UsuariosUnicos = dcount(UserId) by Name
| extend PorcentajeError = round(100.0 * Fallidas / Peticiones, 2)
| order by P95Ms desc
  • count() cuenta filas; countif(condición) cuenta solo las que cumplen algo, evitando una segunda consulta.
  • avg() da el promedio, que miente con las latencias: unas pocas peticiones de 20 segundos apenas mueven la media. percentile(DurationMs, 95) es el valor por debajo del cual queda el 95 % de las peticiones: la métrica honesta, y la que sostiene el SLO de 07-01.
  • dcount() cuenta valores distintos de forma aproximada —algoritmo probabilístico, error en torno al 1 %—, lo que lo hace rapidísimo sobre miles de millones de filas; dcount(UserId, 4) sube la precisión a costa de recursos.
  • by Name agrupa por operación: una fila por punto de conexión.

Para ver la evolución en el tiempo hace falta bin(), que redondea cada marca temporal a un múltiplo del intervalo:

AppRequests
| where TimeGenerated > ago(12h) and AppRoleName == "app-contoso-reservas-pro"
| summarize P95 = percentile(DurationMs, 95), Errores = countif(Success == false)
  by bin(TimeGenerated, 5m)
| render timechart

bin(TimeGenerated, 5m) agrupa todo lo ocurrido en el mismo tramo de 5 minutos; el resultado es una serie temporal. render timechart la dibuja en el portal —también existen barchart, columnchart, piechart y areachart—. Y top 10 by P95 desc es el atajo de order by más take.

  1. Combinar y transformar: join, union, let, parse y mv-expand

let declara constantes y subconsultas reutilizables —let ventana = 2h; o let apps = dynamic(["app-contoso-reservas-pro", "app-contoso-api-disponibilidad-pro"]);, luego usable con where AppRoleName in (apps)—: hace legibles las consultas largas y es imprescindible en la investigación del apartado 7. union apila tablas con esquemas distintos —las columnas que faltan en una quedan vacías—: union AppExceptions, ContainerLogV2 mezcla excepciones de aplicación y salida de contenedores en una sola línea de tiempo. Dentro de un union, $table devuelve de qué tabla venía cada fila y coalesce() toma el primer valor no vacío entre varias columnas equivalentes.

join cruza dos conjuntos por una clave común. Sus tipos:

Tipo Qué devuelve Uso típico en Contoso
innerunique (por defecto) Coincidencias, deduplicando la clave del lado izquierdo Rara vez el que quieres
inner Todas las combinaciones coincidentes Peticiones con sus dependencias
leftouter Todo lo de la izquierda, con lo de la derecha si existe Reservas con o sin tarjeta emitida
rightouter / fullouter Simétricos del anterior Conciliaciones
leftanti Lo de la izquierda que no tiene pareja Reservas sin tarjeta: el caso de este módulo
leftsemi / rightsemi Filtra sin traer columnas del otro lado Comprobar existencia

El innerunique por defecto es la fuente número uno de resultados desconcertantes en KQL: deduplica silenciosamente y los recuentos salen más bajos de lo esperado. Escribe siempre el tipo de forma explícita.

let reservas = AppRequests
    | where TimeGenerated > ago(6h) and Name == "POST /api/reservas" and Success == true
    | project Localizador = tostring(Properties["localizador"]), HoraReserva = TimeGenerated;
let tarjetas = AppTraces
    | where TimeGenerated > ago(6h) and Message startswith "TarjetaEmitida"
    | project Localizador = tostring(Properties["localizador"]);
reservas
| join kind=leftanti tarjetas on Localizador
| order by HoraReserva asc

Esta consulta responde a la pregunta del negocio con una precisión que ninguna métrica podría dar: qué localizadores se pagaron y no tienen tarjeta. leftanti conserva exactamente las filas de la izquierda sin pareja a la derecha.

Texto y estructuras: parse y mv-expand

Por último, parse extrae campos de una cadena sin escribir expresiones regulares:

ContainerLogV2
| where TimeGenerated > ago(1h) and ContainerName == "motor-disponibilidad"
| parse LogMessage with * "vuelo=" Vuelo:string " asientos=" Asientos:int " ms=" Duracion:int *
| where Duracion > 1500
| summarize Consultas = count(), MediaMs = avg(Duracion) by Vuelo
| top 10 by MediaMs desc

El patrón de parse se lee literalmente: el * inicial ignora lo que haya antes, luego busca el texto vuelo=, captura hasta asientos= en Vuelo, y así sucesivamente. Si el formato del mensaje cambia, la captura devuelve vacío en lugar de fallar, así que conviene validarlo.

Su compañero es mv-expand, que convierte un campo con varios valores —normalmente un JSON anidado leído con todynamic()— en varias filas, una por elemento. Una reserva con tres tramos produce tres filas, y así se pueden agregar rutas aunque el dato viniera anidado; lo aplicarás en el ejercicio 3.

  1. La investigación completa de una tarjeta que no llegó

Este es el recorrido que abrió el módulo. El pasajero da su localizador, XR7742. Cinco pasos encadenados.

flowchart LR
  Q["Queja: XR7742"] --> P1["1. AppRequests<br/>¿se registró la compra?"] --> P2["2. AppDependencies<br/>¿se encoló?"]
  P2 --> P3["3. Función<br/>¿se ejecutó?"] --> P4["4. AppExceptions<br/>¿qué error?"] --> P5["5. Key Vault<br/>causa raíz"]

Paso 1: encontrar la compra y su identificador de operación.

AppRequests
| where TimeGenerated > ago(3d) and Properties["localizador"] == "XR7742"
| project TimeGenerated, Name, Success, DurationMs, OperationId, AppRoleName

OperationId es la clave de todo: identifica la operación de negocio completa y se propaga entre componentes. Lo verás en detalle en 07-03.

Paso 2: seguir esa operación por todos los componentes.

let op = toscalar(AppRequests
    | where TimeGenerated > ago(3d) and Properties["localizador"] == "XR7742"
    | top 1 by TimeGenerated desc | project OperationId);
union AppRequests, AppDependencies, AppTraces, AppExceptions
| where TimeGenerated > ago(3d) and OperationId == op
| project TimeGenerated, Tipo = $table, AppRoleName, Detalle = coalesce(Name, Message, ExceptionType)
| order by TimeGenerated asc

toscalar() reduce una subconsulta a un valor único reutilizable, y el union ordenado por tiempo produce la línea de tiempo completa de esa reserva: la petición web, la llamada a sql-contoso-reservas-pro, la escritura en cola-emision-tarjetas y lo que ocurriera después.

Paso 3: comprobar si la función llegó a ejecutarse. Basta con filtrar AppRequests por AppRoleName == "func-contoso-tarjetas-pro" y Name == "GenerarTarjetaEmbarque" con el mismo localizador. Si no devuelve filas, el mensaje se encoló pero nadie lo procesó y el problema está en el desencadenador; si las devuelve con Success == false, sigue el paso 4.

Pasos 4 y 5: el error y su causa raíz.

AppExceptions
| where TimeGenerated > ago(3d) and AppRoleName == "func-contoso-tarjetas-pro"
| summarize Ocurrencias = count(), Localizadores = make_set(tostring(Properties["localizador"]), 10)
  by ExceptionType, OuterMessage
| order by Ocurrencias desc

make_set(columna, N) recoge hasta N valores distintos en una lista: revela de golpe si el fallo afecta solo a XR7742 o a doscientas reservas más. Si la excepción es de acceso denegado, la causa está a un paso, en los registros de auditoría de Key Vault:

AzureDiagnostics
| where TimeGenerated > ago(3d) and ResourceProvider == "MICROSOFT.KEYVAULT"
| where OperationName == "SecretGet" and ResultSignature != "OK"
| project TimeGenerated, id_s, identity_claim_appid_g, ResultSignature

El desenlace real de Contoso: el certificado de firma de PDF de kv-contoso-pro había caducado, la identidad administrada id-contoso-api-pro recibía un error al recuperarlo, la función fallaba tras cinco reintentos y el mensaje acababa en la cola de mensajes fallidos. Cinco consultas, de la queja a la causa raíz, sin abrir una sola sesión en un servidor.

  1. Funciones guardadas y alertas de búsqueda de registros

Ese recorrido no debe reinventarse cada vez. Una función guardada convierte una consulta en un operador nuevo, con parámetros, disponible para todo el equipo:

// Cuerpo guardado en log-contoso-pro con el alias SeguirLocalizador y parámetros (loc:string, dias:int = 3)
let op = toscalar(AppRequests
    | where TimeGenerated > ago(dias * 1d) and Properties["localizador"] == loc
    | top 1 by TimeGenerated desc | project OperationId);
union AppRequests, AppDependencies, AppTraces, AppExceptions
| where TimeGenerated > ago(dias * 1d) and OperationId == op
| project TimeGenerated, Tipo = $table, AppRoleName, Detalle = coalesce(Name, Message, ExceptionType)
| order by TimeGenerated asc

Con la función guardada en la categoría «Contoso - Operaciones», cualquiera del grupo Contoso-Operaciones escribe SeguirLocalizador("XR7742") y obtiene el recorrido completo. Es la diferencia entre que la investigación dependa de Marta Ríos y que la haga el primero que atienda la llamada. Estas funciones y las consultas de ejemplo compartidas se versionan en el repositorio contoso-infra, junto a los libros y las plantillas Bicep del módulo 5.

Alertas de búsqueda de registros

Cualquier consulta KQL que devuelva filas puede convertirse en una alerta. Es lo que permite alertar sobre cosas que ninguna métrica de plataforma expresa:

az monitor scheduled-query create \
  --name alerta-tarjetas-no-emitidas \
  --resource-group rg-contoso-seguridad-pro --scopes "$LOG_ID" \
  --condition "count 'Filas' > 5" \
  --condition-query Filas='AppRequests | where Name == "POST /api/reservas" and Success == true | extend L = tostring(Properties["localizador"]) | join kind=leftanti (AppTraces | where Message startswith "TarjetaEmitida" | extend L = tostring(Properties["localizador"])) on L' \
  --evaluation-frequency 15m --window-size 30m --severity 2 \
  --action-groups ag-equipo-contoso \
  --tags entorno=produccion proyecto=contoso-reservas centro-coste=CC-1042 propietario=marta.rios

Tres decisiones. Una --window-size mayor que la frecuencia da margen a la latencia de ingesta —los datos tardan de uno a cinco minutos en estar disponibles, y una ventana ajustada genera falsos negativos—. --evaluation-frequency 15m en lugar de 1m porque estas alertas se facturan por regla y frecuencia. Y gravedad 2 con ag-equipo-contoso: cinco tarjetas sin emitir es un problema real, pero no justifica un SMS de madrugada.

  1. Consultas entre áreas y exportación de datos

Aunque Contoso use una sola área, hay dos formas de salir de ella. workspace("log-contoso-millas").AppRequests apunta a otra área por nombre o identificador y se combina con union para consultar varias a la vez; resource("/subscriptions/.../sites/app-contoso-reservas-pro") consulta los registros de un recurso concreto sin nombrar el área, y es la forma natural de trabajar en contexto de recurso.

La exportación de datos del área envía tablas completas y en continuo a una cuenta de almacenamiento o a un Event Hubs, sin escribir código. Contoso exporta AzureActivity y AppServiceHTTPLogs a stlagocontosopro, capa bronce: cumplimiento a siete años a precio de almacenamiento en frío y disponibilidad para Synapse (módulo 3) sin volver a pagar la ingesta. La combinación que hay que interiorizar es esa: Log Analytics para los últimos 30-90 días, que es cuando se investiga; almacenamiento barato para el resto.

  1. Planes de tabla, retención y las tres palancas del coste

Aquí está la advertencia central del módulo. La ingesta y la retención en Log Analytics es una de las facturas que más sorprende en Azure. No hay ningún aviso: activas unos registros de depuración durante una investigación, se te olvida desactivarlos, y treinta días después la línea de Log Analytics supera a la del cómputo que la genera. Cada tabla tiene un plan que fija qué se puede hacer con ella y cuánto cuesta:

Plan Precio de ingesta Consultas Alertas Retención Para qué
Analytics El completo KQL sin restricciones Sí Interactiva hasta 2 años Lo que investigas y vigilas
Basic Muy reducido KQL limitado, sobre una sola tabla No Interactiva corta, 30 días Registros voluminosos y de bajo valor
Auxiliary El más bajo Muy limitadas y más lentas No Larga Cumplimiento y auditoría en volumen

La retención, además, se divide en dos tramos: la interactiva (consultable de inmediato, más cara) y el archivo a largo plazo (mucho más barato, hasta 12 años, del que hay que «rehidratar» o consultar con trabajos de búsqueda). Con eso ya se pueden accionar las tres palancas del coste, en orden de impacto:

  1. Qué se ingiere. La palanca dominante; los registros de depuración y de consola son los grandes culpables. Las transformaciones en las DCR permiten descartar en el momento de la ingesta los latidos, los sondeos de estado y las peticiones a recursos estáticos, que no aportan nada y son la mitad del volumen.
  2. En qué plan. Mover ContainerLogV2 a Basic cuando solo se usa para leer los últimos días recorta drásticamente la factura de la tabla más voluminosa de Contoso.
  3. Cuánto tiempo se guarda. La retención se fija por tabla, no solo por área: AzureActivity a 365 días por cumplimiento, AppTraces a 30.

La tabla Usage es gratuita y dice exactamente dónde se va el dinero:

Usage
| where TimeGenerated > ago(30d) and IsBillable == true
| summarize GB = round(sum(Quantity) / 1024, 2) by DataType
| extend EuroAprox = round(GB * 2.30, 2)   // precio ilustrativo por GB
| top 15 by GB desc

Quantity viene en megabytes, de ahí la división. El coste estimado es orientativo —el precio real depende del plan y del compromiso—, pero basta para ordenar por impacto. En Contoso, esta consulta reveló que AppServiceConsoleLogs era el 41 % del volumen total y no lo consultaba nadie. Cambiando el summarize por by bin(TimeGenerated, 1d), DataType y renderizando en columnchart se ve además el día exacto en que algo se disparó. Aplicar el recorte:

WS="--workspace-name log-contoso-pro --resource-group rg-contoso-seguridad-pro"
az monitor log-analytics workspace table update $WS --name AppTraces \
  --retention-time 30 --total-retention-time 30   # 30 días interactivos, sin archivo
az monitor log-analytics workspace table update $WS --name ContainerLogV2 --plan Basic

Por último, el modelo de precios del área: pago por gigabyte frente a compromiso de capacidad. A partir de unos 100 GB al día, comprometer un nivel diario fijo aplica un descuento notable, y a partir de ahí crece por escalones. La regla de Contoso: medir tres meses con Usage, recortar primero lo que no se consulta, y solo después comprometer capacidad sobre el volumen ya optimizado. Comprometer capacidad sobre datos basura es pagar un descuento por basura. Un límite diario de ingesta actúa además como red de seguridad ante un bucle de registro descontrolado, aunque hay que asumir que, alcanzado el tope, se dejan de recibir datos hasta el día siguiente: no lo pongas en las tablas de seguridad.

Errores Comunes y Consejos

  • No filtrar por tiempo al principio, o usar search en producción. Son los dos errores que más cuestan en rendimiento: pon siempre where TimeGenerated > ago(...) como primer operador y parte de una tabla concreta, porque search escanea todas.
  • Dejar el join por defecto. innerunique deduplica en silencio y produce recuentos incorrectos. Escribe kind= siempre.
  • Alertar sobre una ventana igual a la frecuencia. La latencia de ingesta te hará perder eventos: la ventana debe ser mayor.
  • Confundir promedio con percentil. El promedio de latencia oculta la cola larga, que es la que ve el pasajero.
  • Olvidar registros de depuración activados. El origen número uno de facturas sorpresa: revisa Usage cada mes y ponlo en el calendario.
  • Consejo: usa | take 10 mientras desarrollas y quítalo al final; iterar sobre 10 filas es instantáneo. Y prefiere las tablas dedicadas (--export-to-resource-specific), que migran fuera de AzureDiagnostics con mejor esquema y planes por tabla.
  • Consejo: guarda como función toda consulta que hayas escrito dos veces. El conocimiento de investigación debe estar en el área, no en el historial de alguien.

Ejercicios

Ejercicio 1. Escribe una consulta KQL que, sobre la última semana, devuelva por cada operación de app-contoso-api-disponibilidad-pro el número de peticiones, el porcentaje de error y el percentil 95 de la duración, mostrando solo las operaciones con más de 1.000 peticiones y p95 superior a 800 ms. Explica cada línea.

Ejercicio 2. La factura mensual de log-contoso-pro ha pasado de 400 a 1.750 euros sin que se haya desplegado nada nuevo. Describe cómo investigarlo con KQL y propón cuatro medidas concretas, indicando qué palanca acciona cada una.

Ejercicio 3. Nuria Peña quiere un informe semanal con las cinco rutas más reservadas y su ingreso medio, para el proyecto Contoso Millas (centro-coste=CC-2077). Los datos están en eventos personalizados con un campo tramos anidado. Escribe la consulta y explica cómo la entregarías de forma recurrente.

Soluciones

Solución 1:

AppRequests
| where TimeGenerated > ago(7d) and AppRoleName == "app-contoso-api-disponibilidad-pro"
| summarize Peticiones = count(), Fallidas = countif(Success == false),
            P95 = percentile(DurationMs, 95) by Name
| extend PorcentajeError = round(100.0 * Fallidas / Peticiones, 2)
| where Peticiones > 1000 and P95 > 800
| order by P95 desc

El filtro temporal va primero para reducir el volumen escaneado. El segundo where acota a la API. summarize agrega por operación con by Name, usando countif para no necesitar una segunda consulta y percentile en lugar de avg porque el promedio oculta la cola. extend calcula el porcentaje con 100.0 para forzar decimales. El filtro de volumen y latencia va después de summarize, porque opera sobre columnas que no existían antes; ponerlo antes daría error. order by deja arriba lo más grave.

Solución 2: se investiga con Usage, agrupando por DataType y por bin(TimeGenerated, 1d) sobre 60 días y renderizando en columnas; el gráfico muestra el día exacto del salto y qué tabla lo causa. Suele ser una de tres cosas: registros de depuración activados en una investigación y olvidados, una configuración de diagnóstico nueva desplegada por la asignación diagnostico-app-service sobre recursos recién creados, o un bucle de excepciones que genera miles de trazas por minuto. Contrastar con AzureActivity qué cambió esos días. Medidas: (1) desactivar las categorías de diagnóstico que nadie consulta —palanca «qué se ingiere», la de mayor impacto—; (2) añadir una transformación en la DCR que descarte sondeos de estado y latidos —misma palanca, en el origen—; (3) pasar ContainerLogV2 a plan Basic —palanca «en qué plan»—; (4) bajar la retención de AppTraces a 30 días manteniendo AzureActivity a 365 —palanca «cuánto tiempo»—. Y un límite diario de ingesta como red de seguridad, nunca sobre tablas de seguridad.

Solución 3:

AppEvents
| where TimeGenerated > ago(7d) and Name == "ReservaConfirmada" and tostring(Properties["proyecto"]) == "contoso-millas"
| extend Tramos = todynamic(tostring(Properties["tramos"])), Importe = todouble(Properties["importe"])
| mv-expand Tramo = Tramos
| extend Ruta = strcat(tostring(Tramo.origen), "-", tostring(Tramo.destino))
| summarize Reservas = count(), IngresoMedio = round(avg(Importe), 2) by Ruta
| top 5 by Reservas desc

mv-expand desdobla cada tramo en su propia fila y strcat compone la ruta legible. Entrega recurrente: la consulta se guarda como función en log-contoso-pro, se incrusta en un libro parametrizado con el intervalo como parámetro, y ese libro se envía automáticamente cada lunes. La automatización del envío se resuelve con un runbook de Azure Automation o una Logic App —la comparación entre ambas opciones es exactamente el tema de la lección 07-04—. Etiquetas del recurso implicado: entorno, proyecto=contoso-millas, centro-coste=CC-2077 y propietario.

Conclusión

Ya sabes qué es un área de trabajo de Log Analytics y por qué log-contoso-pro es el punto único donde converge todo: registros de recurso, registro de actividad, telemetría de aplicación y alertas de seguridad, con la ventaja decisiva de poder cruzarlos en una sola consulta. Conoces el criterio para separar o unificar áreas, y la pieza que hace viable el área única —el contexto de recurso frente al contexto de área, con enableLogAccessUsingOnlyResourcePermissions, para que cada equipo vea solo lo suyo con el RBAC que ya tiene—, además del mapa de las tablas que te vas a encontrar, con la recomendación de preferir las dedicadas a AzureDiagnostics. Sobre todo, sabes KQL: filtrar con where y los operadores de tiempo, seleccionar con project, calcular con extend y case, agregar con summarize usando count, countif, avg, percentile y dcount, construir series temporales con bin() y dibujarlas con render, combinar con union, let y los seis tipos de join —con la advertencia sobre innerunique—, y extraer estructura con parse y mv-expand. Lo has aplicado al recorrido de investigación completo que abrió el módulo: de la queja del localizador XR7742 a un certificado caducado en kv-contoso-pro, en cinco consultas encadenadas, y lo has convertido en la función compartida SeguirLocalizador para que la investigación no dependa de una sola persona. Has construido una alerta de búsqueda de registros sobre una consulta, alerta-tarjetas-no-emitidas, y sabes exportar datos a stlagocontosopro para conservar barato lo que ya no investigas.

Y llevas la lección de dinero, que es la que más veces se aprende tarde: los planes de tabla Analytics, Basic y Auxiliary, la retención interactiva frente al archivo a largo plazo, y las tres palancas —qué se ingiere, en qué plan y cuánto tiempo se guarda— con la tabla Usage como instrumento para saber dónde se va el dinero antes de tocar nada, y el compromiso de capacidad solo después de haber recortado. Queda un hueco. Todas las consultas del apartado 7 se apoyaban en OperationId, en tablas AppRequests y AppDependencies, y en propiedades como localizador que alguien tuvo que emitir desde el código. Ese dato no aparece por arte de magia: lo genera Application Insights, y de él trata la lección siguiente —el modelo de telemetría, la instrumentación de app-contoso-reservas-pro y func-contoso-tarjetas-pro, la correlación distribuida con el contexto de traza del W3C, el mapa de la aplicación, el muestreo y la telemetría personalizada que convierte una reserva confirmada en un dato consultable—.

Curso de Azure

Módulo 1: Introducción a Azure

Módulo 2: Servicios principales de Azure

Módulo 3: Bases de datos de Azure

Módulo 4: Seguridad en Azure

Módulo 5: Azure DevOps

Módulo 6: Servicios avanzados de Azure

Módulo 7: Monitoreo y gestión

Módulo 8: Gestión y optimización de costos

Módulo 9: Estudios de caso y mejores prácticas

© Copyright 2026. Todos los derechos reservados