Requisitos mínimos de un logger propio y checklist de pruebas de integración
· Actualizado el: · Go Komura · Desarrollo en Windows, Registro de logs, Pruebas de integración, Diseño de pruebas, Confiabilidad
Si se puede usar un logging framework ya existente, esa es la opción más segura. Aun así, hay situaciones en las que las restricciones de la aplicación o las circunstancias operativas no permiten evitar un logger propio. Y ahí es donde surge la primera duda: hasta dónde hay que implementar para lograr un diseño que no sea ni descuidado ni excesivamente pesado.
En este artículo limitamos el alcance al log de aplicación usado para investigar fallos. Sin cargar de una vez con el rastro de auditoría, el trazado distribuido, la infraestructura de métricas o la agregación en la nube, primero definimos una configuración mínima que sea útil en el día a día, y luego organizamos los criterios de pruebas de integración necesarios para poder confiar realmente en esa configuración.
Público objetivo y las premisas de este artículo
| Elemento | Contenido |
|---|---|
| Público objetivo | Desarrolladores que incorporan su propio log de diagnóstico en aplicaciones o herramientas de negocio. Está pensado para equipos pequeños sin un responsable dedicado a la infraestructura de logs |
| Alcance del diseño | No depende del lenguaje. En cualquier entorno donde se pueda añadir contenido a un archivo, el criterio es el mismo tanto en C# como en C++ |
| Ejemplos de código | Se muestran en C# 12 / .NET 8 y en PowerShell 7. En otros lenguajes se pueden reproducir siguiendo el mismo orden y los mismos criterios |
| Log tratado | El log de diagnóstico usado para aislar fallos de la aplicación |
| Fuera de alcance | Rastro de auditoría, trazado distribuido, infraestructura de métricas, agregación en la nube |
Términos usados en este artículo
Antes de entrar en el diseño, resumimos aquí los términos que aparecerán sin explicación adicional en los siguientes apartados.
| Término | Significado |
|---|---|
JSON Lines (.jsonl) |
Formato de texto en el que se escribe un valor JSON por línea, separados por saltos de línea (\n). La codificación es UTF-8 y está establecido que no debe llevar BOM1 |
fields estructurados |
Un contenedor de pares clave-valor con los datos que se quieren poder buscar, separado del texto de message. Tiene una forma como {"file":"orders.csv","row":128} |
single writer |
Diseño en el que solo un punto (un hilo) escribe realmente en el archivo. Sin importar cuántos hilos llamen, se reduce a un único punto de escritura |
bounded queue |
Una cola con un límite superior. El lado que llama solo encola y regresa de inmediato; la escritura la realiza el single writer. Como tiene un límite, hace falta decidir la política para cuando se desborda |
drain |
Escribir hasta el final, al terminar, todos los logs que aún quedan en la cola. Es el proceso que evita que “se encoló pero se perdió” |
flush |
Volcar el búfer que está en memoria hacia el archivo real. Los logs que no pasan por aquí se pierden si hay un cierre anómalo |
| Rotación (rotation) | Cambiar a un archivo nuevo cuando el archivo actual crece demasiado o cambia la fecha |
| Retención (retention) | El límite de cuántos archivos de log antiguos, o hasta cuántos días, se conservan |
Confirmar primero la opción de “no construir uno propio”
Como se mencionó al principio, si se puede usar un logging framework ya existente, esa es la opción más segura. Para poder decidir, aquí van nombres concretos. Si con alguno de ellos basta, no hace falta leer el resto de este artículo.
| Entorno | Opción | Qué trae incorporado desde el inicio |
|---|---|---|
| .NET | Microsoft.Extensions.Logging |
La API ILogger estándar de .NET. Incluye niveles de log (de Trace a Critical), categorías y un mecanismo de proveedores para intercambiar el destino de salida. Viene como referencia implícita en muchos SDK de .NET2 |
| .NET | Serilog | Log de diagnóstico pensado desde el inicio para eventos estructurados. Asigna nombre a los parámetros de la plantilla del mensaje y conserva sus valores como propiedades del evento3 |
| .NET | NLog | Admite tanto el log estructurado como el log tradicional. Tiene un layout JSON como formato de salida, y la salida a archivo incluye nombrado automático y archivado4 |
| C++ | spdlog | Biblioteca de logging utilizable desde C++11. Ofrece salida a archivo de tipo rotating (cambia según el tamaño) y daily (cambia según la fecha)5 |
Cuando existen circunstancias que impiden usar estas opciones (no se pueden agregar dependencias, el entorno de ejecución es limitado, restricciones del código existente, etc.), es cuando entran en juego los requisitos mínimos que siguen.
Conclusión por adelantado
Estos son los puntos clave que conviene fijar en la primera versión:
- El formato es
UTF-8enJSON Lines - No romper la relación de un registro por línea
- Los campos obligatorios son
hora,nivel,categoría,mensaje,fieldsestructurados,sessionIdyprocessId - Lo básico es
1 proceso = 1 archivo - Con carga baja, escritura síncrona; con carga alta,
single writer + bounded queue - Aplicar flush síncrono a
Error/Criticaly al inicio/fin de sesión - Incluir la rotación y la retención desde v1
- No desviar silenciosamente los logs a otro lugar cuando el destino no está disponible
Reduciendo el alcance hasta este punto, resulta difícil que el diseño falle tanto en la implementación como en la operación.
Primero, reducir el alcance
Un logger propio suele complicarse porque, desde el principio, se intenta abarcar todo. Si se pretende reunir en un solo mecanismo el log de diagnóstico, el log de auditoría, la medición de rendimiento, el trazado distribuido y el análisis del comportamiento del usuario, los requisitos se disparan de golpe.
En este caso, el objeto es el log de diagnóstico usado para aislar fallos de la aplicación. Es decir, se prioriza poder rastrear después “cuándo”, “en qué proceso”, “qué ocurrió” y “en qué contexto ocurrió”. Con solo reducir el alcance a esto, las primeras decisiones de diseño se vuelven bastante más sencillas.
Requisitos mínimos necesarios
1. El formato es UTF-8 JSON Lines
Concatenar texto plano también permite conservar el log, pero luego resulta difícil de procesar mecánicamente. Por el contrario, si desde el inicio se usa un formato binario propio y pesado, la observabilidad en producción se resiente.
El punto intermedio, fácil de manejar, es UTF-8 en JSON Lines. Si cada línea es un registro, resulta legible como texto y fácil de analizar después con scripts o herramientas. También es práctico porque, aunque la escritura se corte a la mitad, es fácil aislar qué línea quedó dañada.
Lo único que está definido a nivel de formato son estos tres puntos.1
- Cada línea es un único valor JSON válido (no se incluyen líneas vacías)
- El separador de línea es
\n - La codificación es
UTF-8. No debe llevarBOM(U+FEFF)
La extensión habitual es .jsonl. La prohibición del BOM es fácil de pasar por alto, pero tiene un impacto real. Si se escribe con BOM, solo la primera línea falla al analizarse en otras herramientas, y el problema aparece de una forma confusa: “solo la primera línea está rota”.
2. Fijar los campos obligatorios desde el principio
Los campos mínimos que conviene tener son estos siete:
timestamplevelcategorymessagefieldssessionIdprocessId
Un registro real quedaría, por ejemplo, así (aquí aparece partido en varias líneas por razones de espacio, pero en el archivo real es una sola línea sin saltos).
{"ts":"2026-04-02T01:15:03.4821567Z","level":"Error","category":"import","message":"Error al importar los datos","fields":{"file":"orders.csv","row":128,"reason":"date parse failed"},"sessionId":"20260402-101500-8412-9f3c1d2a5b7e4f689a0c3d5e7f1b2c4d","pid":8412}
Si se colocan en orden el antes y el después dentro de la misma sesión, se ve así.
{"ts":"2026-04-02T01:15:00.1002233Z","level":"Info","category":"startup","message":"Aplicación iniciada","fields":{"version":"1.4.2"},"sessionId":"20260402-101500-8412-9f3c1d2a5b7e4f689a0c3d5e7f1b2c4d","pid":8412}
{"ts":"2026-04-02T01:15:03.4821567Z","level":"Error","category":"import","message":"Error al importar los datos","fields":{"file":"orders.csv","row":128,"reason":"date parse failed"},"sessionId":"20260402-101500-8412-9f3c1d2a5b7e4f689a0c3d5e7f1b2c4d","pid":8412}
{"ts":"2026-04-02T01:15:03.9900011Z","level":"Info","category":"shutdown","message":"Finalizando la aplicación","fields":{"exitCode":1},"sessionId":"20260402-101500-8412-9f3c1d2a5b7e4f689a0c3d5e7f1b2c4d","pid":8412}
Los nombres de las claves pueden ser cortos, pero lo importante es no cambiarlos una vez decididos. Si a mitad de camino se mezclan ts y timestamp, los scripts de análisis que se escriban después se complican de golpe.
Si el log se reduce a una cadena en message, más adelante se complica cuando aumentan las condiciones de búsqueda. Por el contrario, si se agregan demasiados campos, la carga del lado que llama sube de golpe. Es más seguro fijar esto al principio y considerar añadir campos solo cuando realmente haga falta.
Qué poner en sessionId
Puesto que figura entre los campos obligatorios, hay que decidir de antemano qué unidad representa sessionId. En este artículo, una sesión equivale a un arranque del proceso. No es la sesión de inicio de sesión del usuario ni una “transacción” de negocio.
Con esta definición se puede hacer lo siguiente:
- Extraer de una sola vez solo los logs generados en un arranque concreto
- Aunque la rotación divida los archivos, se pueden volver a enlazar después los logs del mismo arranque
- Aislar sin mezclar dos fallos ocurridos en el mismo equipo, uno por la mañana y otro por la tarde
El identificador se decide una sola vez al arrancar el proceso y se reutiliza hasta que este termina. Cualquiera de los dos métodos siguientes es suficiente.
| Método | Ejemplo | Situación adecuada |
|---|---|---|
Hora de arranque + ID de proceso + GUID |
20260402-101500-8412-9f3c1d2a5b7e4f689a0c3d5e7f1b2c4d |
Esta es la opción por defecto. La parte inicial es legible para las personas y la parte final evita colisiones |
Solo UUID (GUID) |
9f0a1c72-3b58-4f2a-9a2e-6e7c1f0d55b1 |
Cuando después se van a reunir en un solo lugar los logs de varios equipos, y no hace falta leerlo a simple vista |
No se conforme con usar solo la hora de arranque más el ID de proceso. El sistema operativo reutiliza el processId. Si el proceso se reinicia dentro del mismo segundo por un bucle de fallos (crash loop), o si la hora local retrocede, el sessionId puede terminar siendo exactamente el mismo que la vez anterior. Si el diseño también usa ese mismo valor en el nombre del archivo, en el momento en que se abre en modo de anexado los logs de arranques distintos se mezclan en un solo archivo, y la premisa “1 arranque = 1 sesión” se rompe en silencio. Además, como la forma en que se rompe es silenciosa, no se nota después.
Mantener processId como campo aparte sirve para diferenciar el uso: sessionId indica “qué arranque fue” y processId indica “cuál era la entidad del sistema operativo en ese momento”.
3. Usar 1 proceso = 1 archivo como base
Un diseño en el que varios procesos añaden líneas al mismo archivo tiene más factores de incidente de los que aparenta, porque el control de exclusión, las escrituras parciales, el momento de la rotación y el manejo de los cierres anómalos se complican todos de golpe.
Empiece usando 1 proceso = 1 archivo como base. Si necesita consolidar varios procesos, es más seguro agregarlos en una etapa posterior o levantar explícitamente un proceso de agregación dedicado.
4. Elegir la estrategia de escritura según la carga
Mientras el volumen de logs sea bajo, la escritura síncrona es más fácil de entender y facilita la investigación de fallos. Forzar la asincronía puede hacer que se pierdan los logs justo antes de terminar, o que las condiciones de flush ante una excepción queden ambiguas.
Por otro lado, si el volumen aumenta y la E/S síncrona se convierte en el cuello de botella, se adopta single writer + bounded queue. Es decir, el lado que llama solo encola en una cola con límite y regresa de inmediato, y solo un punto escribe en el archivo. Esta idea en sí no es rara: incluso las guías de diseño de logging de .NET recomiendan no escribir directamente en un destino lento, sino encolar de forma síncrona en una cola en memoria y enviarlo desde un proceso en segundo plano.2
Lo importante aquí es decidir de antemano la política para cuando la cola se desborda. No deje ambiguo si se descartan los logs antiguos, si se pierden los nuevos o si se emite una advertencia.
5. Decidir las condiciones de flush
Aplicar flush síncrono a Error y Critical, además de a los logs de inicio y fin de sesión, ayuda mucho en la investigación de fallos. Si se hace flush también de todos los Info habituales, el sistema se vuelve lento, así que lo realista es no tratar todo por igual.
6. Incluir la rotación y la retención desde v1
Se suele pensar que la rotación “se puede añadir después”, pero es una función que, una vez en producción, causa problemas de golpe. El método puede ser cualquiera —por tamaño, diario, por cada arranque—, pero al menos debería estar decidido que “no crece indefinidamente” y “cuántos archivos se conservan”.
7. No usar un guardado alternativo silencioso cuando falla el guardado
Un diseño que, cuando el destino de guardado del log no está disponible, escribe en silencio en otro lugar, dificulta la investigación más adelante. El solo hecho de que el responsable de operaciones no encuentre el log “en el lugar donde debería estar” retrasa la respuesta inicial ante un incidente.
Si no se puede guardar, exponga el fallo mediante un medio explícito y claro: una notificación de la aplicación, el registro de eventos, la salida de error estándar, etc. Como mínimo, debería evitarse el estado de “no se sabe adónde fue”.
Imagen de la configuración mínima de v1
En la primera versión, a menudo basta con algo como lo siguiente.
UTF-8 JSON Lines- 1 proceso = 1 archivo
- Nombre de archivo por sesión
- Rotación basada en tamaño o por cada arranque
- Límite en el número de archivos conservados
- Flush síncrono de
Error/Critical - Una API que pueda recibir
fieldsestructurados
Añadir funciones más allá de esto solo después de que aparezca, en la operación real, algo que “de verdad cause problemas” resulta, al final, más fácil de mantener.
Cómo queda la parte de escritura de v1 escrita en C#
Si de los requisitos anteriores solo se implementan el formato, los campos obligatorios, el single writer y las condiciones de flush, el resultado tiene aproximadamente este tamaño (C# 12 / .NET 8). La rotación y la retención se dejan fuera a propósito, bajo la premisa de añadirlas en una etapa posterior.
using System.Text;
using System.Text.Json;
public sealed class JsonLinesLogger : IDisposable
{
// Conserva el japonés (u otro texto no ASCII) tal cual. El codificador
// predeterminado convierte todo lo no ASCII a \uXXXX
private static readonly JsonSerializerOptions JsonOptions = new()
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
private static readonly IReadOnlyDictionary<string, object?> NoFields =
new Dictionary<string, object?>();
private readonly object _gate = new(); // Protege el single writer con este lock
private readonly StreamWriter _writer;
private readonly string _sessionId;
private readonly int _processId = Environment.ProcessId;
public JsonLinesLogger(string path, string sessionId)
{
_sessionId = sessionId;
// JSON Lines prohíbe el BOM, así que se especifica un UTF-8 que no lo escriba
_writer = new StreamWriter(
new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read),
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
}
public void Write(
string level,
string category,
string message,
IReadOnlyDictionary<string, object?>? fields = null)
{
var record = new Dictionary<string, object?>
{
["ts"] = DateTimeOffset.UtcNow.ToString("o"),
["level"] = level,
["category"] = category,
["message"] = message,
["fields"] = fields ?? NoFields, // Nunca lo deja en null: siempre completa los 7 campos
["sessionId"] = _sessionId,
["pid"] = _processId,
};
// Se serializa a JSON antes de escribir, para que un message con saltos
// de línea no rompa la línea
var line = JsonSerializer.Serialize(record, JsonOptions);
lock (_gate)
{
_writer.WriteLine(line);
if (level is "Error" or "Critical")
{
_writer.Flush(); // Solo los logs críticos se vuelcan de forma síncrona
}
}
}
public void Dispose()
{
lock (_gate)
{
_writer.Flush(); // Garantiza que todo se escriba al terminar de forma normal
_writer.Dispose();
}
}
}
El lado que llama queda así. El sessionId se decide una sola vez al arrancar, y se usa el mismo valor también en el nombre del archivo.
// Usa la clase JsonLinesLogger definida antes
var startedAt = DateTimeOffset.Now;
// No basta con la hora y el PID solamente. Si el proceso se reinicia dentro
// del mismo segundo por un bucle de fallos, o si la hora local retrocede,
// combinado con un PID reutilizado por Windows puede quedar el mismo ID que
// la vez anterior. Como JsonLinesLogger abre con FileMode.Append, en ese
// caso los registros de arranques distintos se mezclan en un solo archivo,
// y la premisa "1 arranque = 1 sesión" se rompe en silencio
var sessionId = $"{startedAt:yyyyMMdd-HHmmss}-{Environment.ProcessId}-{Guid.NewGuid():N}";
var logDir = @"C:\ProgramData\MyApp\logs";
Directory.CreateDirectory(logDir);
using var logger = new JsonLinesLogger(Path.Combine(logDir, $"app-{sessionId}.jsonl"), sessionId);
logger.Write("Info", "startup", "Aplicación iniciada",
new Dictionary<string, object?> { ["version"] = "1.4.2" });
logger.Write("Error", "import", "Error al importar los datos",
new Dictionary<string, object?> { ["file"] = "orders.csv", ["row"] = 128 });
Es poco código, pero contiene todo lo que se decidió.
| Lo que se decidió | Dónde se aplica |
|---|---|
UTF-8 sin BOM |
UTF8Encoding(encoderShouldEmitUTF8Identifier: false) |
No convertir el japonés (u otro texto no ASCII) en \uXXXX |
JavaScriptEncoder.UnsafeRelaxedJsonEscaping6 |
| 1 registro = 1 línea | No se concatena message directamente; se llama a WriteLine con el resultado de JsonSerializer.Serialize |
| Incluir los 7 campos obligatorios siempre | Aunque no se especifique fields, se usa NoFields para que nunca falte |
single writer |
Solo se toca _writer dentro de lock (_gate) |
Condiciones de flush |
Flush() inmediato solo para Error / Critical; al terminar, Dispose() siempre llama a Flush() |
Como UnsafeRelaxedJsonEscaping no escapa <, >, & ni ', esta salida no debe incrustarse tal cual en una página HTML ni en un elemento script.6 Utilícela únicamente para leerla como archivo de log.
Errores habituales que se deben evitar
También conviene enumerar los ejemplos típicos que hay que evitar.
- Meter todo dentro de la cadena
message - Compartir el mismo archivo entre varios procesos
- Hacer todo asíncrono sin decidir las condiciones de flush
- Dejar la rotación y la retención para después
- Desviar en silencio a otra carpeta cuando falla el guardado
- Incluir en v1 el envío por red o el guardado en una base de datos local
Todos parecen convenientes a primera vista, pero son puntos que tienden a hacer más pesados el diagnóstico y la operación.
Pensar las pruebas de integración con archivos, hilos y procesos reales
El logger es un componente difícil de dar por confiable solo con pruebas unitarias. Verificar únicamente el formateo de cadenas o la serialización a JSON no basta, porque lo que causa problemas en producción es la E/S, la concurrencia, la rotación, el flush al terminar y los errores de permisos.
Por eso, en las pruebas de integración se verifica con archivos reales, hilos reales y, si hace falta, procesos reales. Como mínimo, conviene evitar el estado de “funciona en condiciones normales, pero no se puede confiar en él durante un fallo”.
Elementos de prueba de integración que conviene pasar
Solidez de la escritura individual
- Si cada línea es un registro JSON
- Si se puede volver a leer en
UTF-8 - Si los campos obligatorios están presentes cada vez
- Si no se rompe en varias líneas por la aparición de saltos de línea
Ejecución concurrente dentro del mismo proceso
- Si los registros no se corrompen al escribir simultáneamente desde varios hilos
- Si no falta ni sobra ningún registro
- Si, al usar una cola, el orden y la política de pérdidas siguen la especificación
Comportamiento de flush y al terminar
- Si
Error/Criticalse reflejan de inmediato - Si la cola queda vacía al terminar normalmente
- Si el log de cierre necesario se conserva incluso en rutas cercanas a un cierre por excepción
Rotación y retención
- Si al cumplirse la condición de rotación se cambia a un archivo nuevo
- Si los archivos antiguos que superan el límite de retención se eliminan según la especificación
- Si las líneas JSON no se rompen justo antes o después de la rotación
Casos anómalos
- El comportamiento cuando el directorio de destino no existe
- El comportamiento cuando no hay permiso de escritura
- La notificación o el valor de retorno cuando falla por algo equivalente a disco lleno
- El comportamiento cuando la cola se desborda
Manejo de varios procesos
Si la especificación es 1 proceso = 1 archivo, se puede verificar precisamente que ningún otro proceso intente entrar en el mismo archivo. Por el contrario, con un esquema de proceso de agregación, hace falta verificar también los fallos en la entrega hacia ese proceso.
Cómo detectar una “rotura”
Con solo enumerar los criterios no se pueden escribir las pruebas. De los puntos anteriores, lo que más suele generar dudas al implementar es cómo determinar que “el registro no está roto”. Verlo a simple vista nunca es suficiente, así que se verifican mecánicamente estos tres puntos.
| Qué se observa | Método de verificación | Rotura que detecta |
|---|---|---|
| Número de líneas | Si el número de escrituras coincide con el número de líneas del archivo | Pérdidas, escrituras duplicadas, fugas de drain |
| Cada línea | Si todas las líneas se pueden analizar individualmente como JSON | Saltos de línea intercalados, interrupciones de escritura, presencia de BOM |
| Cada registro | Si los 7 campos obligatorios están completos cada vez | Campos olvidados, presencia de null |
“Con mirar las primeras 10 líneas parece que está bien” prácticamente no sirve de nada en las pruebas de escritura concurrente. Porque la rotura siempre ocurre en alguna línea del medio. Tiene sentido verificar todas las líneas.
Escrito en PowerShell 7, estos tres puntos se pueden verificar juntos así. Está pensado para llamarse desde el postprocesamiento de la prueba.
# Verifica todas las líneas del log generado. Si una sola está rota, lanza throw y aborta
$path = 'C:\ProgramData\MyApp\logs\app-20260402-101500-8412-9f3c1d2a5b7e4f689a0c3d5e7f1b2c4d.jsonl'
$expected = 10000 # Número de veces que el test llamó a Write
# Con "que exista el nombre de la propiedad" no basta. Un registro como
# `"level": null` también tiene la propiedad presente, así que comprobar
# solo la presencia del nombre deja pasar el caso. Para detectar la
# presencia de null hace falta revisar también el valor y el tipo
$required = [ordered]@{
'ts' = { param($v) $v -is [string] -and $v -ne '' }
'level' = { param($v) $v -is [string] -and $v -ne '' }
'category' = { param($v) $v -is [string] -and $v -ne '' }
'message' = { param($v) $v -is [string] } # Permite cadena vacía, pero no null
'fields' = { param($v) $v -is [pscustomobject] }
'sessionId' = { param($v) $v -is [string] -and $v -ne '' }
'pid' = { param($v) ($v -is [int] -or $v -is [long]) -and $v -gt 0 }
}
# La verificación del BOM se hace con bytes crudos, antes de decodificar.
# Get-Content -Encoding utf8 descarta el BOM inicial antes de devolver las
# líneas, así que por más que se mire la cadena ya decodificada, nunca se
# detecta la presencia de un BOM
# (en 5.1, en lugar de -AsByteStream se usa -Encoding Byte)
$head = @(Get-Content -LiteralPath $path -AsByteStream -TotalCount 3)
if ($head.Count -ge 3 -and $head[0] -eq 0xEF -and $head[1] -eq 0xBB -and $head[2] -eq 0xBF) {
throw 'El archivo tiene un BOM UTF-8 al principio. Es inválido como JSON Lines'
}
$lines = @(Get-Content -LiteralPath $path -Encoding utf8)
if ($lines.Count -ne $expected) {
throw "El número de líneas no coincide. Esperadas: $expected / reales: $($lines.Count)"
}
$lineNo = 0
foreach ($line in $lines) {
$lineNo++
try {
$record = $line | ConvertFrom-Json
}
catch {
throw "La línea $lineNo no se puede leer como JSON: $line"
}
$names = $record.PSObject.Properties.Name
foreach ($name in $required.Keys) {
if ($names -notcontains $name) {
throw "La línea $lineNo no tiene el campo obligatorio: $name"
}
if (-not (& $required[$name] $record.$name)) {
$shown = if ($null -eq $record.$name) { '(null)' } else { "'$($record.$name)'" }
throw "El valor de $name en la línea $lineNo no es válido: $shown"
}
}
}
"OK: las $($lines.Count) líneas se leyeron correctamente, una línea = un registro"
Con esta forma, se puede reutilizar tal cual en las siguientes pruebas.
- Escritura concurrente: escribir
nveces desde varios hilos y pasar la prueba con$expectedigual an - Rotación y retención: aplicar la misma verificación a todos los archivos que quedan tras la rotación, cotejando el número de líneas por el total
drainal terminar: verificar después de intercalar el proceso de cierre, comprobando si coincide con el número de elementos encolados
Los casos anómalos no se pueden medir de esta forma, así que se revisan aparte. Se prepara antes la condición —por ejemplo, que no exista el directorio de destino o que no haya permiso de escritura— y luego se inicializa el logger, comprobando que el fallo salga a la luz mediante una excepción, un valor de retorno o una notificación. Si aquí la implementación es de las que “no pasa nada y el error se traga en silencio”, ese es el tipo de rotura que más problemas causa en producción.
El número mínimo de pruebas que conviene pasar en v1
Si se intenta hacer todo desde el principio, las pruebas se vuelven demasiado pesadas. Lo mínimo que conviene pasar en v1 son estas seis, aproximadamente:
- Escritura normal en un solo hilo
- Escritura simultánea desde varios hilos
- Flush de
Error/Critical - Rotación y retención
- Notificación de fallo cuando el destino está en mal estado
- Drain y flush final al terminar normalmente
Con solo pasar estas seis pruebas, ya se está bastante lejos de tener “un logger que produce texto, pero en el que no se puede confiar en producción”.
Resumen
El primer objetivo de un logger propio no es acumular funciones, sino “poder confiar en él durante un fallo”. Para lograrlo, resulta efectivo fijar el formato en UTF-8 JSON Lines, reducir los campos obligatorios, usar 1 proceso = 1 archivo como base y decidir pronto el flush, la rotación, la retención y el comportamiento ante fallos.
Y si ese diseño realmente funciona o no es algo que debe verificarse con pruebas de integración que usen archivos, hilos y procesos reales. Antes de ampliar la implementación, fijar primero la configuración mínima y el conjunto mínimo de pruebas hace que después sea más fácil hacerla crecer sin forzarla.
Referencias
-
JSON Lines, JSON Lines ↩ ↩2
-
Microsoft Learn, Logging in C# - .NET ↩ ↩2
-
gabime, spdlog - Fast C++ logging library ↩
-
Microsoft Learn, How to customize character encoding with System.Text.Json ↩ ↩2
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Cómo trazar el límite entre pruebas unitarias y pruebas de integración
Analizamos el límite entre pruebas unitarias y de integración: lógica pura, formato, cableado, entorno y tiempo, en una tabla de decisión...
Tabla de decisión: terminar o continuar ante una excepción inesperada
Analiza si una aplicación debe terminar o continuar tras una excepción inesperada, según el daño al estado, los efectos externos, los hil...
Cómo funcionan el portapapeles y arrastrar y soltar — Gestionar correctamente la transferencia de datos OLE en aplicaciones empresariales
Una tabla de Excel se deforma al pegarla y deja de poder pegarse si cierra el origen: es el portapapeles colocando el mismo contenido en ...
WPR/WPA en la práctica — Introducción al análisis de rendimiento del sistema para «todo el PC va lento»
Problemas como «todo el PC va lento» o «el arranque es lento», que el Administrador de tareas no explica, se investigan con WPR/WPA leyen...
El apagado de Windows visto desde la aplicación ── cómo sobrevivir correctamente a la notificación de cierre, el reinicio y el corte de energía
Un reinicio nocturno de Windows Update corrompió datos de medición: ese accidente se evita con buen diseño. Explicamos, con fuentes ofici...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Encaja bien con este tema porque ordena el diseño, la implementación y la operación de logs en herramientas y aplicaciones de negocio para Windows, ajustándolos a los requisitos reales del proyecto.
Consultoría técnica y revisión de diseño
Definir de antemano el formato del log, la rotación, el comportamiento ante fallos y el alcance de las pruebas de integración es, en sí mismo, un tema típico de consultoría técnica.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Qué formato debería tener el log de un logger propio?
- Se recomienda UTF-8 en JSON Lines, con un formato que no rompa la relación de un registro por línea. Concatenar texto plano es difícil de procesar mecánicamente más adelante, y un formato binario propio reduce la observabilidad en producción. Con JSON Lines el texto se puede leer directamente, resulta fácil de analizar desde scripts y herramientas, y si la escritura se corta a la mitad es sencillo aislar la línea dañada, lo que lo hace práctico para el trabajo real. Los campos obligatorios se fijan en siete: timestamp, level, category, message, fields estructurados, sessionId y processId.
- ¿La escritura del log debería ser síncrona o asíncrona?
- Depende de la carga. Mientras el volumen de logs sea bajo, la escritura síncrona resulta más simple de entender y facilita la investigación de fallos. Forzar la asincronía puede hacer que se pierdan los logs justo antes de terminar, o que las condiciones de flush ante una excepción queden ambiguas. Si el volumen es alto y la E/S síncrona se convierte en el cuello de botella, conviene adoptar single writer + bounded queue, decidiendo de antemano si, cuando la cola se desborda, se descartan los logs antiguos o se pierden los nuevos. Aplicar flush síncrono a los logs de Error/Critical y a los de inicio y fin de sesión ayuda mucho durante la investigación de fallos.
- ¿Se puede escribir en el mismo archivo de log desde varios procesos?
- Debería evitarse. Un diseño en el que varios procesos añaden líneas al mismo archivo complica de golpe el control de exclusión, las escrituras parciales, el momento de la rotación y el manejo de los cierres anómalos, y genera más factores de incidente de los que parece a simple vista. Lo básico es usar 1 proceso = 1 archivo; si se necesita consolidar varios procesos, es más seguro agregarlos en una etapa posterior o levantar explícitamente un proceso de agregación dedicado.
- ¿Qué se debe comprobar en las pruebas de integración de un logger propio?
- Se comprueba con archivos, hilos y procesos reales. Las pruebas unitarias de formateo de cadenas o de serialización a JSON por sí solas no detectan los problemas que surgen en producción: E/S, concurrencia, rotación, flush al terminar y errores de permisos. Para v1 conviene pasar al menos seis pruebas: escritura normal en un solo hilo, escritura simultánea desde varios hilos, flush de Error/Critical, rotación y retención, notificación de fallo cuando el destino de guardado está en mal estado, y drain con flush final al terminar normalmente. Si estas seis pasan, se está bastante lejos de tener un logger en el que no se puede confiar en producción.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.