< Summary

Information
Class: FAU.Logica.Logging.FauLogFormatter
Assembly: FAU.Logica
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Logging/FauLogFormatter.cs
Line coverage
97%
Covered lines: 133
Uncovered lines: 3
Coverable lines: 136
Total lines: 212
Line coverage: 97.7%
Branch coverage
95%
Covered branches: 63
Total branches: 66
Branch coverage: 95.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
Format(...)94.44%363695%
ResolveContext(...)100%1818100%
ToKebabCase(...)87.5%88100%
GetTemplatePropertyNames(...)100%44100%

File(s)

/home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Logging/FauLogFormatter.cs

#LineLine coverage
 1using Serilog.Events;
 2using Serilog.Formatting;
 3using Serilog.Parsing;
 4using System.Text;
 5
 6namespace FAU.Logica.Logging;
 7
 8/// <summary>
 9/// Formateador de logs en el formato estándar FAU:
 10/// [TIMESTAMP] [LEVEL] [usuario] [contexto] Mensaje {datos}
 11/// </summary>
 12public class FauLogFormatter : ITextFormatter
 13{
 114    private static readonly HashSet<string> MetaProperties = new(StringComparer.OrdinalIgnoreCase)
 115    {
 116        "SourceContext", "RequestId", "RequestPath", "ConnectionId",
 117        "ActionId", "ActionName", "SpanId", "TraceId", "ParentId",
 118        "MachineName", "ThreadId", "ProcessId", "ProcessName", "EventId",
 119        // Propiedad de contexto explícita (mostrada en posición 4, no en datos)
 120        "Context",
 121        // IDs internos de auditoría (reemplazados por nombres descriptivos)
 122        "usuario_id", "accion_id", "contexto_id", "host"
 123    };
 24
 125    private static readonly Dictionary<string, string> ContextMap = new(StringComparer.OrdinalIgnoreCase)
 126    {
 127        // Servicios
 128        ["PersonalService"] = "personal",
 129        ["DependienteService"] = "personal",
 130        ["UsuarioService"] = "usuarios",
 131        ["RolService"] = "roles",
 132        ["AuditoriaService"] = "auditoria",
 133        ["LiquidacionService"] = "liquidacion",
 134        ["NovedadPeriodoService"] = "liquidacion",
 135        ["ParametroLiquidacionService"] = "liquidacion",
 136        ["PeriodoService"] = "liquidacion",
 137        ["PeriodoGuard"] = "liquidacion",
 138        ["InsumoEvaluador"] = "liquidacion",
 139        ["BeneficioSocialService"] = "beneficios-sociales",
 140        ["TipoBeneficioService"] = "beneficios-sociales",
 141        ["CompensacionService"] = "compensaciones",
 142        ["BancoService"] = "bancos",
 143        ["AuthService"] = "autenticacion",
 144        // Controladores
 145        ["PersonalController"] = "personal",
 146        ["DependienteController"] = "personal",
 147        ["UsuariosController"] = "usuarios",
 148        ["RolesController"] = "roles",
 149        ["AuditoriaController"] = "auditoria",
 150        ["LiquidacionesController"] = "liquidacion",
 151        ["NovedadesPeriodoController"] = "liquidacion",
 152        ["ParametrosLiquidacionController"] = "liquidacion",
 153        ["PeriodosController"] = "liquidacion",
 154        ["BeneficiosSocialesController"] = "beneficios-sociales",
 155        ["TiposBeneficioController"] = "beneficios-sociales",
 156        ["CompensacionesController"] = "compensaciones",
 157        ["BancosController"] = "bancos",
 158        ["AuthController"] = "autenticacion",
 159        ["CatalogoItemsController"] = "sistema",
 160        ["RegimenesController"] = "sistema",
 161        ["TestController"] = "sistema",
 162        ["Auditoria"] = "sistema",
 163    };
 64
 65    public void Format(LogEvent logEvent, TextWriter output)
 66    {
 67        // 1. Timestamp ISO 8601 UTC con milisegundos
 3768        var ts = logEvent.Timestamp.UtcDateTime;
 3769        output.Write('[');
 3770        output.Write(ts.ToString("yyyy-MM-ddTHH:mm:ss.fff"));
 3771        output.Write("Z]");
 72
 73        // 2. Level en mayúsculas
 3774        output.Write(" [");
 3775        output.Write(logEvent.Level switch
 3776        {
 177            LogEventLevel.Debug => "DEBUG",
 3278            LogEventLevel.Information => "INFO",
 179            LogEventLevel.Warning => "WARN",
 280            LogEventLevel.Error => "ERROR",
 181            LogEventLevel.Fatal => "FATAL",
 082            _ => logEvent.Level.ToString().ToUpperInvariant()
 3783        });
 3784        output.Write(']');
 85
 86        // 3. Usuario (opcional - solo si está autenticado)
 3787        if (logEvent.Properties.TryGetValue("Username", out var userProp) &&
 3788            userProp is ScalarValue { Value: string uname } &&
 3789            !string.IsNullOrWhiteSpace(uname))
 90        {
 391            output.Write(" [");
 392            output.Write(uname);
 393            output.Write(']');
 94        }
 95
 96        // 4. Contexto en kebab-case
 3797        output.Write(" [");
 3798        output.Write(ResolveContext(logEvent));
 3799        output.Write(']');
 100
 101        // 5. Mensaje
 37102        output.Write(' ');
 37103        logEvent.MessageTemplate.Render(logEvent.Properties, output);
 104
 105        // 6. Datos extra (propiedades que no están en el template ni son meta)
 37106        var templateNames = GetTemplatePropertyNames(logEvent.MessageTemplate);
 37107        var extras = logEvent.Properties
 32108            .Where(kv => !MetaProperties.Contains(kv.Key)
 32109                      && kv.Key != "Username"
 32110                      && !templateNames.Contains(kv.Key))
 37111            .ToList();
 112
 37113        if (extras.Count > 0)
 114        {
 4115            output.Write(" {");
 4116            bool first = true;
 18117            foreach (var kv in extras)
 118            {
 119                // "detalle" se expande inline para evitar el wrapper {detalle: {...}}
 5120                if (kv.Key == "detalle" && kv.Value is StructureValue sv)
 121                {
 12122                    foreach (var prop in sv.Properties)
 123                    {
 6124                        if (!first) output.Write(", ");
 4125                        first = false;
 4126                        output.Write(prop.Name);
 4127                        output.Write(": ");
 4128                        prop.Value.Render(output);
 129                    }
 130                }
 131                else
 132                {
 4133                    if (!first) output.Write(", ");
 3134                    first = false;
 3135                    output.Write(kv.Key);
 3136                    output.Write(": ");
 3137                    kv.Value.Render(output);
 138                }
 139            }
 4140            output.Write('}');
 141        }
 142
 143        // 7. Excepción
 37144        if (logEvent.Exception != null)
 145        {
 1146            output.WriteLine();
 1147            output.Write("  ");
 1148            output.Write(logEvent.Exception.GetType().FullName);
 1149            output.Write(": ");
 1150            output.Write(logEvent.Exception.Message);
 1151            if (logEvent.Exception.StackTrace is { } st)
 152            {
 0153                output.WriteLine();
 0154                output.Write(st);
 155            }
 156        }
 157
 37158        output.WriteLine();
 37159    }
 160
 161    private static string ResolveContext(LogEvent logEvent)
 162    {
 163        // Prioridad 1: propiedad Context explícita (ej: auditoria con ContextoEnum)
 37164        if (logEvent.Properties.TryGetValue("Context", out var ctxProp) &&
 37165            ctxProp is ScalarValue { Value: string explicitCtx } &&
 37166            !string.IsNullOrWhiteSpace(explicitCtx))
 167        {
 2168            return explicitCtx;
 169        }
 170
 171        // Prioridad 2: SourceContext (nombre de la clase que loguea)
 35172        if (logEvent.Properties.TryGetValue("SourceContext", out var sc) &&
 35173            sc is ScalarValue { Value: string sourceContext })
 174        {
 14175            var className = sourceContext.Split('.').Last();
 14176            if (ContextMap.TryGetValue(className, out var ctx))
 13177                return ctx;
 178
 179            // Fallback: remover sufijo y convertir a kebab-case
 1180            className = className
 1181                .Replace("Controller", "")
 1182                .Replace("Service", "");
 1183            if (!string.IsNullOrEmpty(className))
 1184                return ToKebabCase(className);
 185        }
 21186        return "sistema";
 187    }
 188
 189    private static string ToKebabCase(string pascal)
 190    {
 1191        if (string.IsNullOrEmpty(pascal)) return pascal;
 1192        var sb = new StringBuilder();
 28193        for (int i = 0; i < pascal.Length; i++)
 194        {
 13195            if (i > 0 && char.IsUpper(pascal[i]))
 1196                sb.Append('-');
 13197            sb.Append(char.ToLower(pascal[i]));
 198        }
 1199        return sb.ToString();
 200    }
 201
 202    private static HashSet<string> GetTemplatePropertyNames(MessageTemplate template)
 203    {
 37204        var names = new HashSet<string>(StringComparer.Ordinal);
 152205        foreach (var token in template.Tokens)
 206        {
 39207            if (token is PropertyToken pt)
 2208                names.Add(pt.PropertyName);
 209        }
 37210        return names;
 211    }
 212}