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
- Qué es un área de trabajo de Log Analytics
- Diseño del área: una o varias, y RBAC
- Las tablas que te vas a encontrar
- KQL desde cero: filtrar, proyectar y extender
- Agregar:
summarize,bin()y series temporales - Combinar y transformar:
join,union,let,parseymv-expand - La investigación completa de una tarjeta que no llegó
- Funciones guardadas y alertas de búsqueda de registros
- Consultas entre áreas y exportación de datos
- Planes de tabla, retención y las tres palancas del coste
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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.
- 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 conLectorsobreapp-contoso-reservas-devve 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.
- 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.
- 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 20Línea a línea:
AppServiceHTTPLogs— la tabla de origen. Empezar siempre por una tabla concreta, nunca porsearch.where TimeGenerated > ago(1h)— filtro temporal; admite5m,7d,30d. Va el primero siempre: es el que reduce el volumen escaneado y, por tanto, el coste y el tiempo de la consulta.wheresucesivos filtran por sitio y por código de estado:==distingue mayúsculas,=~no.projectselecciona columnas y descarta el resto, reduciendo lo que viaja al navegador;order by ... descordena —sin él el orden no está garantizado— ytake 20corta 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, Familiabetween (datetime(...) .. datetime(...))acota una ventana absoluta, para reconstruir un incidente ya cerrado. Otros operadores de tiempo frecuentes:startofday(now()),startofweek(),now()-2d.extendcreaSegundosRespuestaa partir de milisegundos; el.0fuerza la división decimal.case(...)evalúa condiciones en orden y devuelve el primer acierto; el último valor es el «si no».
- Agregar:
summarize, bin() y series temporales
summarize, bin() y series temporalessummarize 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 desccount()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 Nameagrupa 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 timechartbin(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.
- Combinar y transformar:
join, union, let, parse y mv-expand
join, union, let, parse y mv-expandlet 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 ascEsta 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 descEl 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.
- 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, AppRoleNameOperationId 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 asctoscalar() 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 descmake_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, ResultSignatureEl 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.
- 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 ascCon 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.riosTres 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.
- 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.
- 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:
- 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.
- En qué plan. Mover
ContainerLogV2a Basic cuando solo se usa para leer los últimos días recorta drásticamente la factura de la tabla más voluminosa de Contoso. - Cuánto tiempo se guarda. La retención se fija por tabla, no solo por área:
AzureActivitya 365 días por cumplimiento,AppTracesa 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 descQuantity 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 BasicPor ú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
searchen producción. Son los dos errores que más cuestan en rendimiento: pon siemprewhere TimeGenerated > ago(...)como primer operador y parte de una tabla concreta, porquesearchescanea todas. - Dejar el
joinpor defecto.inneruniquededuplica en silencio y produce recuentos incorrectos. Escribekind=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
Usagecada mes y ponlo en el calendario. - Consejo: usa
| take 10mientras 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 deAzureDiagnosticscon 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 descEl 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 descmv-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
- ¿Qué es Azure?
- Modelos de servicio, regiones y zonas de disponibilidad
- Crear y configurar tu cuenta de Azure
- Recorrido por el portal de Azure
- Azure Resource Manager: suscripciones, grupos de recursos y etiquetas
- Azure CLI, PowerShell y Cloud Shell
Módulo 2: Servicios principales de Azure
- Máquinas virtuales de Azure
- Escalado y alta disponibilidad del cómputo
- Azure App Service
- Azure Storage: blobs, archivos, colas y tablas
- Redes en Azure: redes virtuales, subredes y NSG
- Conectividad híbrida y entrega global
Módulo 3: Bases de datos de Azure
- Elegir el servicio de datos adecuado
- Azure SQL Database
- Azure Cosmos DB
- Azure Database for MySQL
- Azure Database for PostgreSQL
- Analítica de datos: Data Lake, Data Factory y Synapse
Módulo 4: Seguridad en Azure
- Microsoft Entra ID y gestión de identidades
- RBAC e identidades administradas
- Azure Key Vault
- Protección DDoS y firewall de aplicaciones web
- Microsoft Defender for Cloud
- Gobernanza y cumplimiento con Azure Policy
Módulo 5: Azure DevOps
- Introducción a Azure DevOps
- Azure Repos
- Azure Pipelines: integración continua
- Despliegue continuo con entornos y aprobaciones
- Azure Artifacts
- Infraestructura como código con Bicep
Módulo 6: Servicios avanzados de Azure
- Contenedores en Azure: Container Registry y Container Apps
- Azure Kubernetes Service (AKS)
- Azure Functions
- Azure Logic Apps
- Mensajería y eventos: Service Bus, Event Grid y Event Hubs
- Servicios de IA de Azure
Módulo 7: Monitoreo y gestión
- Azure Monitor: métricas, alertas y paneles
- Log Analytics y consultas KQL
- Application Insights
- Azure Automation y runbooks
- Copias de seguridad y recuperación ante desastres
Módulo 8: Gestión y optimización de costos
- Calculadora de precios y estimación de costes
- Azure Cost Management: análisis, presupuestos y alertas
- Reservas, planes de ahorro y Azure Hybrid Benefit
- Azure Advisor
- Estrategias de optimización y cultura FinOps
