Requisitos mínimos de un logger propio y checklist de pruebas de integración

· Actualizado el: · · 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-8 en JSON Lines
  • No romper la relación de un registro por línea
  • Los campos obligatorios son hora, nivel, categoría, mensaje, fields estructurados, sessionId y processId
  • 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 / Critical y 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 llevar BOM (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:

  • timestamp
  • level
  • category
  • message
  • fields
  • sessionId
  • processId

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 fields estructurados

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 / Critical se 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 n veces desde varios hilos y pasar la prueba con $expected igual a n
  • 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
  • drain al 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:

  1. Escritura normal en un solo hilo
  2. Escritura simultánea desde varios hilos
  3. Flush de Error / Critical
  4. Rotación y retención
  5. Notificación de fallo cuando el destino está en mal estado
  6. 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

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

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.

Volver al blog