{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"},{"name":"Jendrik Brack","url":"https://daily-devops.net/authors/jendrik/"}],"description":"Recent content in Structured Logging \u0026 Observability on Daily DevOps \u0026 .NET","favicon":"https://daily-devops.net/images/logo_hu_6465d873dfa490cf.png","feed_url":"https://daily-devops.net/tags/logging/feed.json","home_page_url":"https://daily-devops.net/tags/logging/","icon":"https://daily-devops.net/images/logo_hu_5926de77762241ba.png","items":[{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eI have spent half a day staring at a production incident wondering why I could not correlate any log entries across a single request. Everything looked fine. \u003ccode\u003eILogger\u003c/code\u003e was there, \u003ccode\u003eBeginScope\u003c/code\u003e was called, the structured properties were in the templates. In development, the console showed exactly what I expected. In production: nothing. No correlation ID. No scope context. Just a flat stream of messages from parallel requests, interleaved, undifferentiated, useless.\u003c/p\u003e\n\u003cp\u003eThe culprit was not a bug. It was me not understanding what \u003ccode\u003eILogger\u003c/code\u003e actually is: a façade with a lot of opt-in behaviour that looks enabled by default.\u003c/p\u003e\n\u003cp\u003eThat incident cost me half a day. I have seen variants of it in nearly every codebase I have worked in since. The patterns are always the same: a developer who trusts that logging works because it compiles, and finds out in production that it does not.\u003c/p\u003e\n\u003cp\u003eThis is a tour of the ways \u003ccode\u003eILogger\u003c/code\u003e lies to you, and by that I mean: the ways its defaults and abstractions let you believe things are working when they are not. These are not obscure edge cases. Most of them are the default configuration.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"lie-1-your-log-message-is-evaluated-before-the-level-check\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#lie-1-your-log-message-is-evaluated-before-the-level-check\" title=\"Lie 1: Your Log Message Is Evaluated Before the Level Check\"\u003eLie 1: Your Log Message Is Evaluated Before the Level Check\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis one is easy to miss because it never crashes. It just silently costs you.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;Processing order {order.Id} with {order.Items.Count} items totaling {order.Total:C}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIf Debug is filtered out (which it is, in every default production configuration), the string interpolation still runs. \u003ccode\u003eorder.Items.Count\u003c/code\u003e is evaluated. The currency format is applied. Memory is allocated for the full interpolated string. Then the whole thing is thrown away.\u003c/p\u003e\n\u003cp\u003eYou will not notice this until you profile something. And then you will find Debug-level log calls in your hot path costing you measurable throughput, silently, in production.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-message-templates-beat-interpolation\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#why-message-templates-beat-interpolation\" title=\"Why Message Templates Beat Interpolation\"\u003eWhy Message Templates Beat Interpolation\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe fix is message templates, not because they are more readable, but because the parameters are not evaluated until after the level check:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogDebug\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Processing order {OrderId} with {ItemCount} items totaling {Total}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eItems\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eorder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotal\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe structured fields are also preserved as separate properties rather than baked into a string, which matters for Lie 4.\u003c/p\u003e\n\u003cp\u003eFor anything called frequently: \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/core/extensions/logger-message-generator\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eLoggerMessage source generators\u003c/a\u003e. Zero allocation when filtered, correct property types, generated at compile time.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kd\"\u003epartial\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eLogMessages\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [LoggerMessage(Level = LogLevel.Debug, Message = \u0026#34;Processing order {OrderId} with {ItemCount} items totaling {Total}\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kd\"\u003epartial\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eProcessingOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eitemCount\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003edecimal\u003c/span\u003e \u003cspan class=\"n\"\u003etotal\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is what \u003ccode\u003eMicrosoft.Extensions.Logging\u003c/code\u003e source generators exist for. If you are not using them in hot paths, you are paying for logging that is disabled.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"lie-2-your-log-scopes-are-probably-not-appearing\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#lie-2-your-log-scopes-are-probably-not-appearing\" title=\"Lie 2: Your Log Scopes Are Probably Not Appearing\"\u003eLie 2: Your Log Scopes Are Probably Not Appearing\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis is the one that cost me half a day.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBeginScope\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;OrderId: {OrderId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Starting payment processing\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Sending confirmation email\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe premise is clean: every log call inside the \u003ccode\u003eusing\u003c/code\u003e block carries \u003ccode\u003eOrderId\u003c/code\u003e. When you have hundreds of parallel requests hitting a service, this is how you keep them separate in your logs. Without it, you get an undifferentiated stream where tracing a single request means grepping for an ID you may or may not have logged consistently.\u003c/p\u003e\n\u003cp\u003eIn development, the console shows the scope. You trust it. You ship it.\u003c/p\u003e\n\u003cp\u003eIn production, \u003ccode\u003eBeginScope\u003c/code\u003e returns a disposable that does nothing. No error. No warning. The scope is dropped entirely.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-beginscope-returns-a-no-op\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#why-beginscope-returns-a-no-op\" title=\"Why BeginScope Returns A No-Op\"\u003eWhy BeginScope Returns A No-Op\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe reason is that scope support is opt-in per provider. \u003ccode\u003eAddConsole()\u003c/code\u003e supports scopes but does not include them in output unless you enable it:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;Logging\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;Console\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nt\"\u003e\u0026#34;IncludeScopes\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAnd that is just the console. Every production sink (Application Insights, Seq, Elasticsearch) has its own scope configuration, its own opt-in. If your sink\u0026rsquo;s provider does not implement \u003ccode\u003eIExternalScopeConsumer\u003c/code\u003e, the \u003ccode\u003eSetScopeProvider\u003c/code\u003e call never happens, and every \u003ccode\u003eBeginScope\u003c/code\u003e you call is a no-op at the provider level.\u003c/p\u003e\n\u003cp\u003eI found this out by looking at the sink source code after half a day of adding increasingly desperate debug logging. The fix was one line in the sink configuration. The knowledge that I needed to add that line existed nowhere near the \u003ccode\u003eBeginScope\u003c/code\u003e documentation.\u003c/p\u003e\n\u003cp\u003eBefore you depend on scope data for incident correlation: query your actual production logs for a scope property. Verify it is there as a separate field, not missing entirely.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"lie-3-your-minimum-level-configuration-has-contradictions-you-have-not-noticed\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#lie-3-your-minimum-level-configuration-has-contradictions-you-have-not-noticed\" title=\"Lie 3: Your Minimum Level Configuration Has Contradictions You Have Not Noticed\"\u003eLie 3: Your Minimum Level Configuration Has Contradictions You Have Not Noticed\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe default \u003ccode\u003eappsettings.json\u003c/code\u003e logging section looks sane enough:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;Logging\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;LogLevel\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nt\"\u003e\u0026#34;Default\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Information\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nt\"\u003e\u0026#34;Microsoft\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Warning\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nt\"\u003e\u0026#34;Microsoft.Hosting.Lifetime\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Information\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe behavior is a longest-prefix match. \u003ccode\u003eMicrosoft.Hosting.Lifetime\u003c/code\u003e beats \u003ccode\u003eMicrosoft\u003c/code\u003e because it is more specific. The order inside the configuration object is irrelevant. Fine.\u003c/p\u003e\n\u003cp\u003eNow add Serilog (which most production .NET applications do at some point):\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;Serilog\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;MinimumLevel\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nt\"\u003e\u0026#34;Default\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Information\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"nt\"\u003e\u0026#34;Override\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"nt\"\u003e\u0026#34;Microsoft\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Warning\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eYou now have two independent filter systems. Both must pass. Your \u003ccode\u003eMicrosoft.Hosting.Lifetime: Information\u003c/code\u003e override in \u003ccode\u003eLogging:LogLevel\u003c/code\u003e has no equivalent in Serilog, so Serilog\u0026rsquo;s \u003ccode\u003eMicrosoft: Warning\u003c/code\u003e blocks it.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"useserilog-bypasses-your-loglevel-section\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#useserilog-bypasses-your-loglevel-section\" title=\"UseSerilog Bypasses Your LogLevel Section\"\u003eUseSerilog Bypasses Your LogLevel Section\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eBut here is the part that genuinely surprised me: when you use \u003ccode\u003eUseSerilog()\u003c/code\u003e in your host setup, the \u003ccode\u003eLogging\u003c/code\u003e section in \u003ccode\u003eappsettings.json\u003c/code\u003e is bypassed entirely. Serilog replaces the entire MEL provider. Your \u003ccode\u003eLogLevel\u003c/code\u003e configuration (the one you have been editing, the one that looks like it should be in charge) is not read at all. Only the \u003ccode\u003eSerilog\u003c/code\u003e section matters.\u003c/p\u003e\n\u003cp\u003eI have seen people spend significant time adjusting the \u003ccode\u003eLogging:LogLevel\u003c/code\u003e configuration in a codebase where \u003ccode\u003eUseSerilog()\u003c/code\u003e was in \u003ccode\u003eProgram.cs\u003c/code\u003e. Every change had zero effect, and there was no indication why.\u003c/p\u003e\n\u003cp\u003ePick one authoritative minimum level configuration and remove the other. Do not maintain both.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"lie-4-your-structured-properties-are-not-structured\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#lie-4-your-structured-properties-are-not-structured\" title=\"Lie 4: Your Structured Properties Are Not Structured\"\u003eLie 4: Your Structured Properties Are Not Structured\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe whole point of \u003ccode\u003eILogger\u003c/code\u003e with message templates, over plain \u003ccode\u003eConsole.WriteLine\u003c/code\u003e, is that \u003ccode\u003e{OrderId}\u003c/code\u003e becomes a queryable property in your log aggregation system, not a substring buried in a flat string. That is the pitch for structured logging.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogWarning\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Payment failed for {CustomerId} with error {ErrorCode}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomerId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eerrorCode\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWith a correctly configured structured sink, you can query \u003ccode\u003eErrorCode == \u0026quot;INSUFFICIENT_FUNDS\u0026quot;\u003c/code\u003e. That query is indexed. It is fast. It works across millions of entries.\u003c/p\u003e\n\u003cp\u003eWith a plain text sink (or a structured sink with default formatting), you get \u003ccode\u003e\u0026quot;Payment failed for cust-123 with error INSUFFICIENT_FUNDS\u0026quot;\u003c/code\u003e. That is a string. You search it with a substring match. Under load, across millions of entries, you wait.\u003c/p\u003e\n\u003cp\u003eThree things must all be true for structured properties to actually be structured:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eYou use message templates, not string interpolation\u003c/li\u003e\n\u003cli\u003eYour sink supports structured output\u003c/li\u003e\n\u003cli\u003eYour sink is configured to output properties as separate fields\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThat third point is the one that bites you. Application Insights, by default, maps everything into \u003ccode\u003ecustomDimensions\u003c/code\u003e under a single composite key in some configurations. File sinks writing plain text give you a formatted message and nothing else. The console in default mode renders the template as a string.\u003c/p\u003e\n\u003cp\u003eThe practical test is simple: take a log entry from your production aggregation system that uses a structured parameter. Check whether the parameter appears as its own field. If it is embedded in the message string, your structured logging is decorative. It looks right in code and does nothing useful at query time.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"lie-5-exception-logging-loses-the-inner-exception-chain\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#lie-5-exception-logging-loses-the-inner-exception-chain\" title=\"Lie 5: Exception Logging Loses the Inner Exception Chain\"\u003eLie 5: Exception Logging Loses the Inner Exception Chain\u003c/a\u003e\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ecatch\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eException\u003c/span\u003e \u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order processing failed for {OrderId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is correct usage. \u003ccode\u003eILogger\u003c/code\u003e accepts an exception as the first parameter, serializes it, attaches it to the log event. You did everything right.\u003c/p\u003e\n\u003cp\u003eWhat you get in your logs depends entirely on what the sink does with \u003ccode\u003eException.ToString()\u003c/code\u003e versus a structured exception decomposition.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"when-aggregateexception-hides-the-real-failure\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#when-aggregateexception-hides-the-real-failure\" title=\"When AggregateException Hides The Real Failure\"\u003eWhen AggregateException Hides The Real Failure\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe problem is \u003ccode\u003eAggregateException\u003c/code\u003e and friends. \u003ccode\u003eAggregateException: One or more errors occurred.\u003c/code\u003e is the most useless log entry in the .NET ecosystem. It tells you exactly nothing about what actually failed. The real exception is in \u003ccode\u003eInnerException\u003c/code\u003e, one or more levels deep.\u003c/p\u003e\n\u003cp\u003eApplication Insights handles this well, serializing the full exception chain into separate telemetry entries. A plain JSON file sink with default settings often gives you just the outer exception type and message. You stare at \u003ccode\u003eAggregateException\u003c/code\u003e and go spelunking through stack traces.\u003c/p\u003e\n\u003cp\u003eIf you cannot configure your sink\u0026rsquo;s exception serialization depth, make the root cause explicit:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ecatch\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eException\u003c/span\u003e \u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003einnermost\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ewhile\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003einnermost\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInnerException\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003einnermost\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003einnermost\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInnerException\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order processing failed for {OrderId}. Root cause: {RootCause}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eorderId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003einnermost\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is defensive. It makes the useful information explicit in the log message rather than relying on the sink to dig it out of the exception chain.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"lie-6-your-log-timestamps-are-potentially-wrong\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#lie-6-your-log-timestamps-are-potentially-wrong\" title=\"Lie 6: Your Log Timestamps Are Potentially Wrong\"\u003eLie 6: Your Log Timestamps Are Potentially Wrong\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eILogger\u003c/code\u003e does not add timestamps. The sink does. The sink decides what \u0026ldquo;now\u0026rdquo; means and in which timezone it records it.\u003c/p\u003e\n\u003cp\u003eThree places this goes wrong:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eUTC vs local time confusion.\u003c/strong\u003e Your application runs in UTC. Your sink records local time based on the server\u0026rsquo;s system clock. Your aggregation system converts to UTC. Depending on timezone offsets and Daylight Saving Time (DST), you end up with timestamps that are consistently wrong by hours. Correlating logs across services (one in UTC, one in local) means doing timezone arithmetic during an incident.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"clock-skew-across-services\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#clock-skew-across-services\" title=\"Clock Skew Across Services\"\u003eClock Skew Across Services\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eClock skew in distributed systems.\u003c/strong\u003e Multiple services writing to the same aggregation endpoint. Each server\u0026rsquo;s Network Time Protocol (NTP) sync is slightly different, maybe 50ms, maybe 500ms. Log entries that should be sequential appear out of order when sorted by timestamp. You lose the ability to reconstruct event sequences across service boundaries.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eBuffered writes with stale timestamps.\u003c/strong\u003e Some sinks batch writes for throughput. The timestamp attached to the log event is the time of the sink write, not the time of the log call. Under load, that drift can be seconds. You cannot trust the timestamp order to represent the call order.\u003c/p\u003e\n\u003cp\u003eFor Serilog, be explicit about timestamp format and timezone:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eLoggerConfiguration\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteTo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoutputTemplate\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"s\"\u003e\u0026#34;[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] ...\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreateLogger\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAnd verify that \u003ccode\u003eTimestamp\u003c/code\u003e is captured at call time, not write time. For buffered sinks, use a custom enricher to capture \u003ccode\u003eDateTimeOffset.UtcNow\u003c/code\u003e at the point the log method is called if ordering matters to you.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-actually-works\"\u003e\u003ca href=\"/posts/your-ilogger-is-lying-to-you/#what-actually-works\" title=\"What Actually Works\"\u003eWhat Actually Works\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe pattern across all six of these is the same: \u003ccode\u003eILogger\u003c/code\u003e compiles, runs, produces no errors, and silently does something different from what you expected. None of these are bugs. They are documentation you did not read, or opt-in behaviour that looks like a default.\u003c/p\u003e\n\u003cp\u003eI am not being harsh about Microsoft here — this is mostly a user problem. The documentation exists. The configuration options are there. But the defaults are not conservative defaults that fail loudly when misconfigured. They are optimistic defaults that look like they are working until you need them to actually work.\u003c/p\u003e\n\u003cp\u003eA correctly configured logging pipeline:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eUse message templates everywhere. No string interpolation in log calls.\u003c/li\u003e\n\u003cli\u003eUse \u003ccode\u003e[LoggerMessage]\u003c/code\u003e source generators for any log call in a hot path.\u003c/li\u003e\n\u003cli\u003eUse \u003ccode\u003eBeginScope\u003c/code\u003e for correlation context (request ID, user ID, operation ID) — and verify scope support for your specific sink in your specific configuration before you depend on it.\u003c/li\u003e\n\u003cli\u003eConfigure one authoritative minimum level source. Either \u003ccode\u003eLogging:LogLevel\u003c/code\u003e or Serilog\u0026rsquo;s \u003ccode\u003eMinimumLevel\u003c/code\u003e. Not both.\u003c/li\u003e\n\u003cli\u003eWrite at least one integration test that queries actual log output and verifies structured properties appear as separate fields, not embedded in message strings.\u003c/li\u003e\n\u003cli\u003eVerify exception serialization depth for your sink with a deliberately thrown \u003ccode\u003eAggregateException\u003c/code\u003e. Look at what you get.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe only way to know what your logs actually contain is to look at them in production, under real conditions. Development logging lies to you in the opposite direction — it shows you scopes, structured properties, and correct timestamps because the console provider actually works.\u003c/p\u003e\n\u003cp\u003eProduction is where the assumptions fail. Look there.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ccode\u003eILogger\u003c/code\u003e is a façade over a pipeline you did not configure. The pipeline does not care that you trusted the façade.\u003c/p\u003e\n\u003c/blockquote\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-05-21T17:00:00+02:00","id":"https://daily-devops.net/posts/your-ilogger-is-lying-to-you/","language":"en","summary":"Half a day lost to BeginScope silently doing nothing in production. ILogger compiles, runs, produces no errors, and fails quietly in six distinct ways.","tags":["logging","dotnet","csharp","observability","bestpractices","softwareengineering"],"title":"Six Ways ILogger Silently Fails in Production","url":"https://daily-devops.net/posts/your-ilogger-is-lying-to-you/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eA developer runs \u003ccode\u003edotnet ef database update\u003c/code\u003e against production. Fifteen minutes later, the database behaves strangely. Three hours into the incident, someone asks the obvious question: \u0026ldquo;Who ran that migration?\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eSilence.\u003c/p\u003e\n\u003cp\u003eThe terminal window closed. The build log expired last week. Nobody remembers. The migration tool printed \u0026ldquo;Success\u0026rdquo; to the console and promptly forgot everything.\u003c/p\u003e\n\u003cp\u003eThis scenario repeats across organizations constantly. Database migrations, deployment scripts, data cleanup tools, configuration utilities: they all share the same blind spot. They execute privileged operations that modify production systems, then vanish without a trace.\u003c/p\u003e\n\u003cp\u003eYour web applications log every user action. Your APIs track every request. But your CLI tools? They are operating in the shadows, modifying databases, deploying code, deleting records. All without leaving evidence of who did what, when they did it, or whether it even succeeded.\u003c/p\u003e\n\u003cp\u003eThat missing accountability will bite you. During incident investigations, when you need to understand what happened. During security reviews, when you need to prove who had access. During compliance assessments, when auditors ask uncomfortable questions about privileged operations.\u003c/p\u003e\n\u003cp\u003eThe fix is straightforward: treat CLI tools like the privileged applications they are. Structured logging. User identity tracking. Correlation IDs. Persistent storage. The same discipline you apply to production web apps.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-consolewriteline-problem\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#the-consolewriteline-problem\" title=\"The Console.WriteLine Problem\"\u003eThe Console.WriteLine Problem\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eEvery .NET developer has written code like this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eProgram\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003estatic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eMain\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003eargs\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Starting database migration...\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eAppDbContext\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDatabase\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMigrateAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Migration completed successfully\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFunctionally correct. Operationally blind.\u003c/p\u003e\n\u003cp\u003eThree months later, someone asks: \u0026ldquo;Who ran this migration? When exactly? Against which environment?\u0026rdquo; The answers do not exist. Console output died with the terminal session. Build logs expired. PowerShell transcripts only captured interactive sessions, and this ran from a scheduled task.\u003c/p\u003e\n\u003cp\u003eThe pattern repeats everywhere. Deployment tools that \u003ccode\u003eConsole.WriteLine\u003c/code\u003e their progress then forget everything. Admin scripts that run as service accounts, severing any link to the human who triggered them. Data cleanup utilities that delete records (sometimes including the logs that would track such deletions) without leaving a trace.\u003c/p\u003e\n\u003cp\u003eThis is not an edge case. This is the default behavior of virtually every custom CLI tool in enterprise .NET environments. And it creates real problems.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"why-missing-logs-hurt\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#why-missing-logs-hurt\" title=\"Why Missing Logs Hurt\"\u003eWhy Missing Logs Hurt\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe problems surface in predictable scenarios.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eIncident investigations\u003c/strong\u003e stall when nobody can determine what changed. \u0026ldquo;Someone ran something last week\u0026rdquo; is not actionable intelligence. Without timestamps, user identities, and operation details, root cause analysis becomes archaeology instead of forensics.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSecurity reviews\u003c/strong\u003e fail when you cannot demonstrate who has accessed what. Privileged operations require accountability. Service accounts that hide human identity defeat the purpose of access controls.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCompliance assessments\u003c/strong\u003e get uncomfortable when auditors ask about privileged operation logging and you have to explain that your CLI tools print to console and hope someone is watching. Every security framework, from SOC 2 to ISO 27001 to GDPR, requires logging user actions, timestamps, successes, failures, and affected resources. Console output satisfies none of these requirements.\u003c/p\u003e\n\u003cp\u003eThe common thread: you need evidence. Evidence of who did what, when they did it, and what the outcome was. Evidence that persists beyond terminal sessions and build log retention periods. Evidence you can query, filter, and present.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fix-structured-logging-with-identity\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#the-fix-structured-logging-with-identity\" title=\"The Fix: Structured Logging with Identity\"\u003eThe Fix: Structured Logging with Identity\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe solution is not complicated. It requires discipline, not genius. Every CLI tool needs four things: user identity, correlation IDs, structured events, and persistent storage.\u003c/p\u003e\n\u003cp\u003eHere is a practical implementation using \u003ccode\u003eMicrosoft.Extensions.Logging\u003c/code\u003e and Application Insights:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eAuditedCliTool\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eAuditedCliTool\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"n\"\u003eTelemetryClient\u003c/span\u003e \u003cspan class=\"n\"\u003e_telemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003e_correlationId\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eGuid\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNewGuid\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003e_userIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eAuditedCliTool\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eAuditedCliTool\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eTelemetryClient\u003c/span\u003e \u003cspan class=\"n\"\u003etelemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_telemetry\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etelemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003e_userIdentity\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eResolveUserIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eResolveUserIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Try Windows identity first (corporate environments)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ewindowsIdentity\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eWindowsIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetCurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsNullOrEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ewindowsIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ewindowsIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// CI/CD pipelines set this via environment variable\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eciUser\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eEnvironment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetEnvironmentVariable\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;AUDIT_USER_PRINCIPAL\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(!\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIsNullOrEmpty\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eciUser\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eciUser\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// Fallback: machine + username (better than nothing)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s\"\u003e$\u0026#34;{Environment.MachineName}\\\\{Environment.UserName}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eExecuteAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eoperation\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e            [\u0026#34;CorrelationId\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_correlationId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e            [\u0026#34;User\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_userIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e            [\u0026#34;Operation\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eoperation\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e            [\u0026#34;Target\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e            [\u0026#34;Machine\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eEnvironment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMachineName\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBeginScope\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogWarning\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;OPERATION_START: {User} initiated {Operation} on {Target}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003e_userIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eoperation\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003e_telemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTrackEvent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;{operation}Started\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003etry\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003ePerformOperation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoperation\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etarget\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Status\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Success\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogWarning\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;OPERATION_COMPLETE: {Operation} succeeded for {User}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"n\"\u003eoperation\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003e_userIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003e_telemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTrackEvent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;{operation}Completed\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ecatch\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eException\u003c/span\u003e \u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Status\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Failed\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Error\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;OPERATION_FAILED: {Operation} failed for {User}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                    \u003cspan class=\"n\"\u003eoperation\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003e_userIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003e_telemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTrackEvent\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;{operation}Failed\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe key elements deserve explanation.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eUser identity resolution\u003c/strong\u003e tries multiple strategies because CLI tools run in diverse environments. Windows authentication works on corporate networks. Environment variables work in CI/CD pipelines where you control the execution context. The fallback ensures you always capture something, even if it is just the machine name.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCorrelation IDs\u003c/strong\u003e tie everything together. When this CLI tool triggers downstream operations (API calls, database queries, message queue publishes), the correlation ID follows. During incident investigation, you can trace the entire operation flow across systems.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStructured logging with scopes\u003c/strong\u003e attaches context to every log entry. The \u003ccode\u003eBeginScope\u003c/code\u003e call ensures that \u003ccode\u003eCorrelationId\u003c/code\u003e, \u003ccode\u003eUser\u003c/code\u003e, \u003ccode\u003eOperation\u003c/code\u003e, and \u003ccode\u003eTarget\u003c/code\u003e appear on every log line within that block. Log aggregation tools can filter and query these fields.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eDual-channel logging\u003c/strong\u003e sends events to both local logs and Application Insights. If one system fails, you still have records. Application Insights provides real-time querying; local logs provide backup and extended retention.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"cicd-pipelines-the-identity-problem\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#cicd-pipelines-the-identity-problem\" title=\"CI/CD Pipelines: The Identity Problem\"\u003eCI/CD Pipelines: The Identity Problem\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCI/CD pipelines add a wrinkle. Your CLI tool runs, but it runs as \u003ccode\u003egithub-actions[bot]\u003c/code\u003e or whatever service account executes the pipeline. The human who triggered the deployment disappears from the record.\u003c/p\u003e\n\u003cp\u003eThe fix is propagating context. GitHub Actions provides \u003ccode\u003egithub.actor\u003c/code\u003e (the human who triggered the workflow), \u003ccode\u003egithub.run_id\u003c/code\u003e (unique identifier for correlation), and \u003ccode\u003egithub.sha\u003c/code\u003e (the exact code version). Pass these to your CLI tools:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eDeploy with audit context\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003eenv\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eAUDIT_USER_PRINCIPAL\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e${{ github.actor }}@github-actions\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nt\"\u003eAUDIT_CORRELATION_ID\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003e${{ github.run_id }}-${{ github.run_attempt }}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    dotnet run --project DeploymentTool -- \\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e      --environment Production \\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e      --correlation-id \u0026#34;${{ env.AUDIT_CORRELATION_ID }}\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNow your CLI tool receives the actual human identity through the environment variable. The correlation ID links Application Insights telemetry back to the specific GitHub Actions run. You can trace from a production incident to the exact workflow execution and the person who triggered it.\u003c/p\u003e\n\u003cp\u003eFor long-term retention, upload logs as artifacts with extended retention periods. GitHub Actions deletes run logs after 90 days by default. Artifacts can persist for years if you configure \u003ccode\u003eretention-days\u003c/code\u003e appropriately.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"querying-your-logs\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#querying-your-logs\" title=\"Querying Your Logs\"\u003eQuerying Your Logs\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCollecting logs is pointless if you cannot query them. When someone asks \u0026ldquo;Who ran that migration last Tuesday?\u0026rdquo;, you need answers in minutes, not hours of log archaeology.\u003c/p\u003e\n\u003cp\u003eApplication Insights provides KQL (Kusto Query Language) for this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecustomEvents\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| where name == \u0026#34;MigrationCompleted\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| where timestamp \u0026gt; ago(7d)\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| project timestamp,\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          user = customDimensions.User,\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          target = customDimensions.Target,\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e          correlationId = customDimensions.CorrelationId\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| order by timestamp desc\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis query finds all migrations in the last week, showing who ran them and what they targeted. The correlation ID lets you trace related operations across systems.\u003c/p\u003e\n\u003cp\u003eFor incident investigation, start with the correlation ID and expand outward:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003elet targetCorrelation = \u0026#34;abc-123-def\u0026#34;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eunion traces, customEvents, exceptions\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| where customDimensions.CorrelationId == targetCorrelation\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e| order by timestamp asc\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis reconstructs the complete operation timeline: what started, what succeeded, what failed, and in what order. Forensics, not archaeology.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"protecting-your-logs\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#protecting-your-logs\" title=\"Protecting Your Logs\"\u003eProtecting Your Logs\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eLogs that can be modified or deleted are not evidence. They are suggestions. For logs to have value during investigations or compliance reviews, they need protection.\u003c/p\u003e\n\u003cp\u003eApplication Insights provides built-in immutability: you cannot modify or delete individual events. Only entire workspaces can be purged, and only after retention periods expire. For most scenarios, this is sufficient.\u003c/p\u003e\n\u003cp\u003eIf you need stronger guarantees, export to Azure Blob Storage with immutability policies enabled. Once uploaded, even administrators cannot modify or delete the files until the retention period expires. Seven years is typical for regulatory requirements.\u003c/p\u003e\n\u003cp\u003eFor the truly paranoid, cryptographic signatures provide tamper evidence. Hash the log export, sign the hash, store the signature separately. Any modification invalidates the signature, proving tampering occurred. This is overkill for most organizations, but some regulated industries require it.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-checklist\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#the-checklist\" title=\"The Checklist\"\u003eThe Checklist\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eImplementing this is straightforward once you commit to the discipline:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eEvery CLI tool needs:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eStructured logging via \u003ccode\u003eMicrosoft.Extensions.Logging\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eUser identity resolution (Windows, environment variable, fallback)\u003c/li\u003e\n\u003cli\u003eCorrelation IDs for cross-system tracing\u003c/li\u003e\n\u003cli\u003eStart, success, and failure events logged explicitly\u003c/li\u003e\n\u003cli\u003eSensitive data sanitized before logging\u003c/li\u003e\n\u003cli\u003eCentralized log storage (Application Insights, Elasticsearch, whatever you use)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eCI/CD pipelines need:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHuman identity propagated through environment variables\u003c/li\u003e\n\u003cli\u003eWorkflow run IDs used as correlation IDs\u003c/li\u003e\n\u003cli\u003eLog artifacts uploaded with appropriate retention periods\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eLog storage needs:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eRetention periods matching your compliance requirements\u003c/li\u003e\n\u003cli\u003eImmutability policies preventing modification\u003c/li\u003e\n\u003cli\u003eAccess controls limiting who can read sensitive logs\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"the-real-payoff\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#the-real-payoff\" title=\"The Real Payoff\"\u003eThe Real Payoff\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eCompliance requirements drove this article, but compliance is not the real reason to implement proper CLI logging. The real reason is operational sanity.\u003c/p\u003e\n\u003cp\u003eI have investigated production incidents where the only evidence was \u0026ldquo;someone ran something last week.\u0026rdquo; Hours wasted on archaeology when forensics would have taken minutes. Proper logs with user identities, timestamps, correlation IDs, and outcomes transform incident investigation from guesswork into reconstruction.\u003c/p\u003e\n\u003cp\u003eSecurity incident response benefits identically. When your SIEM alerts on suspicious database activity, audit trails immediately answer whether this was a legitimate admin operation or an actual breach. Fast answers depend on complete records.\u003c/p\u003e\n\u003cp\u003eCompliance gives you the justification. Operational excellence is the payoff.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"conclusion\"\u003e\u003ca href=\"/posts/audit-trail-dotnet-cli-tools/#conclusion\" title=\"Conclusion\"\u003eConclusion\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eYour CLI tools are lying to you. They run, they print success, they forget everything.\u003c/p\u003e\n\u003cp\u003eThat amnesia creates real problems during incidents, security reviews, and compliance assessments. The fix is not complicated: structured logging, user identity tracking, correlation IDs, persistent storage. The same discipline you apply to production web applications.\u003c/p\u003e\n\u003cp\u003eThe .NET ecosystem provides everything you need. \u003ccode\u003eMicrosoft.Extensions.Logging\u003c/code\u003e handles structured logging. Application Insights provides centralized storage and querying. GitHub Actions context variables enable identity propagation in CI/CD pipelines.\u003c/p\u003e\n\u003cp\u003eBuild this into your CLI tools from day one. Not as an afterthought when someone asks uncomfortable questions. As a fundamental requirement for any tool that touches production systems.\u003c/p\u003e\n\u003cp\u003eFuture you, investigating a 2 AM incident, will appreciate the evidence.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-04-21T17:00:00+02:00","id":"https://daily-devops.net/posts/audit-trail-dotnet-cli-tools/","language":"en","summary":"dotnet ef database update prints Success and forgets. Add structured logging, user identity, and correlation IDs so privileged CLI runs leave evidence.","tags":["iso-standards","security","cli","dotnet","logging"],"title":"Who Ran That Migration? Audit Trails for .NET CLI Tools","url":"https://daily-devops.net/posts/audit-trail-dotnet-cli-tools/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003e\u0026ldquo;Show me the access logs for user authentication events over the past six months.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eYou grep through text files scattered across three servers, paste fragments into Excel, and spend forty minutes assembling evidence that proves nothing useful. The auditor checks a box. You both know this is theater.\u003c/p\u003e\n\u003cp\u003eThis scene replays in organizations everywhere, and it reveals a fundamental misunderstanding about what audit logging should accomplish. Teams treat logging as a checkbox exercise—something to satisfy compliance requirements rather than infrastructure that actually protects systems and enables incident response.\u003c/p\u003e\n\u003cp\u003eISO 27001 Control A.12.4 exists because security incidents leave traces—if you capture them. Attackers probing authentication endpoints, privilege escalations, unauthorized data exports—these events generate signals. The question is whether your logging infrastructure captures those signals in a form that\u0026rsquo;s actually useful, or whether it buries them in terabytes of unstructured noise.\u003c/p\u003e\n\u003cp\u003eMost implementations fail spectacularly. They log too much, filling disks with DEBUG spam that nobody reads. They log too little, providing no context when something breaks at 2 AM. They log the wrong things—I\u0026rsquo;ve reviewed codebases that captured credit card numbers and API keys in plain text while missing the authentication failures that would have detected an actual breach. And they store logs where application users can delete them, which defeats the entire purpose of audit trails.\u003c/p\u003e\n\u003cp\u003eThe gap between \u0026ldquo;we have logging\u0026rdquo; and \u0026ldquo;we satisfy A.12.4\u0026rdquo; is wider than teams realize. This article closes it using .NET structured logging with Application Insights—not compliance theater, but infrastructure that engineers actually use for troubleshooting while simultaneously satisfying auditors.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-a124-actually-requires\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#what-a124-actually-requires\" title=\"What A.12.4 Actually Requires\"\u003eWhat A.12.4 Actually Requires\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eFour controls. Four things auditors check.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA.12.4.1 (Event logging)\u003c/strong\u003e mandates recording user activities, exceptions, and security events with sufficient context for investigation. When unauthorized access occurs, your logs must answer who did what, when, and where. Vague entries like \u0026ldquo;error occurred\u0026rdquo; provide zero forensic value.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA.12.4.2 (Protection)\u003c/strong\u003e addresses a reality that many teams ignore: attackers who compromise systems will attempt to cover their tracks. Logs stored in the same database that application users access? Non-compliant. Logs writable by the same service account that generates them? Equally problematic. You need immutable storage, separate credentials, and ideally external collection systems that the application itself cannot manipulate.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA.12.4.3 (Privileged operations)\u003c/strong\u003e recognizes that admin accounts represent elevated risk. When a database administrator exports the entire customer table at 3 AM, that event demands capture and review. Regular user activity might aggregate; privileged operations must remain granular and traceable.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eA.12.4.4 (Clock synchronization)\u003c/strong\u003e sounds trivial until you try to reconstruct an incident timeline across distributed services. If your web tier timestamps events in UTC, your database in local time, and your authentication service in server time, correlation becomes guesswork. Consistent NTP synchronization isn\u0026rsquo;t optional.\u003c/p\u003e\n\u003cp\u003eMeeting these requirements doesn\u0026rsquo;t demand expensive SIEM products or complex compliance tools. It demands disciplined engineering.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-fatal-approach-what-fails-audits\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#the-fatal-approach-what-fails-audits\" title=\"The Fatal Approach: What Fails Audits\"\u003eThe Fatal Approach: What Fails Audits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;ve reviewed codebases at three separate organizations over the past eighteen months that shared the same fundamental mistakes. The pattern is depressingly consistent:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;User {request.UserId} attempting order at {DateTime.Now}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;Card {request.CreditCardNumber}, CVV {request.CVV}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eConsole\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWriteLine\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e$\u0026#34;Payment gateway key: {_config[\u0026#34;\u003c/span\u003e\u003cspan class=\"n\"\u003ePaymentGateway\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003eApiKey\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;]}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis code violates every A.12.4 control simultaneously, and I\u0026rsquo;m not exaggerating. The unstructured string concatenation produces logs that are impossible to query. When an auditor requests \u0026ldquo;all access attempts by user ID 42,\u0026rdquo; you hand them a 900 MB text file with instructions to Ctrl+F. That\u0026rsquo;s not compliance—that\u0026rsquo;s evidence of negligence.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eConsole.WriteLine\u003c/code\u003e outputs vanish when containers restart. In production, you might retain three weeks of logs instead of six months because deployments rotate infrastructure. The auditor asks for historical data; you explain that it doesn\u0026rsquo;t exist.\u003c/p\u003e\n\u003cp\u003eThe sensitive data exposure creates immediate PCI-DSS violations alongside the A.12.4.2 failures. Credit card numbers, CVV codes, API keys—all logged in plain text, accessible to thirty developers who have read permissions for debugging purposes. When breach notification requirements trigger, these logs become evidence of negligent data handling.\u003c/p\u003e\n\u003cp\u003eWithout correlation IDs, tracing a single request across distributed services becomes archaeology. The order creation endpoint calls inventory, payment, and notification services. Each generates independent log streams with no shared identifier. Good luck reconstructing what actually happened when something fails.\u003c/p\u003e\n\u003cp\u003eAnd \u003ccode\u003eDateTime.Now\u003c/code\u003e instead of UTC guarantees timestamp chaos. Your US-East servers log in EST, your Europe instances in CET, your database in UTC. Incident timelines become exercises in timezone arithmetic that auditors rightfully reject.\u003c/p\u003e\n\u003cp\u003eThis approach fails audits, fails operations, and fails security. All at once.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-correct-approach-structured-logging\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#the-correct-approach-structured-logging\" title=\"The Correct Approach: Structured Logging\"\u003eThe Correct Approach: Structured Logging\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e.NET\u0026rsquo;s \u003ccode\u003eILogger\u003c/code\u003e interface supports structured logging out of the box, yet most teams use it incorrectly. The critical pattern that separates queryable audit trails from unstructured noise: message templates with semantic properties instead of string interpolation.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003escope\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBeginScope\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eobject\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [\u0026#34;CorrelationId\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eActivity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"n\"\u003eGuid\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNewGuid\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [\u0026#34;UserId\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUserId\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s\"\u003e\u0026#34;Order creation initiated for UserId {UserId} with Amount {Amount}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eTotalAmount\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s\"\u003e\u0026#34;Processing payment, CardLast4 {CardLast4}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eMaskCreditCard\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCreditCardNumber\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Order failed for UserId {UserId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUserId\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe difference between \u003ccode\u003e$\u0026quot;User {userId}\u0026quot;\u003c/code\u003e and \u003ccode\u003e\u0026quot;User {UserId}\u0026quot;, userId\u003c/code\u003e seems cosmetic, but it\u0026rsquo;s fundamental. The first produces opaque text that requires regex parsing. The second captures \u003ccode\u003eUserId\u003c/code\u003e as a structured property that Application Insights indexes automatically. You can query \u003ccode\u003etraces | where customDimensions.UserId == \u0026quot;42\u0026quot;\u003c/code\u003e and retrieve every operation for that user in seconds—no grep, no text parsing, no manual correlation.\u003c/p\u003e\n\u003cp\u003eCorrelation IDs via \u003ccode\u003eActivity.Current\u003c/code\u003e propagate across distributed calls automatically. ASP.NET Core creates an \u003ccode\u003eActivity\u003c/code\u003e for each inbound request, and when you call downstream services using \u003ccode\u003eHttpClient\u003c/code\u003e, that ID flows via headers. Application Insights groups these distributed traces, visualizing the complete request flow across services. This is A.12.4.1 compliance that also makes production debugging possible.\u003c/p\u003e\n\u003cp\u003eRedaction of sensitive data prevents the credential exposure that plagues most codebases. The \u003ccode\u003eMaskCreditCard\u003c/code\u003e helper preserves last-four digits for support purposes while removing PAN data that triggers PCI-DSS scope. API keys, passwords, tokens—none appear in logs. You capture context without creating liability.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"application-insights-setup\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#application-insights-setup\" title=\"Application Insights Setup\"\u003eApplication Insights Setup\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddApplicationInsightsTelemetry\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eoptions\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConnectionString\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econfig\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;ApplicationInsights:ConnectionString\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eoptions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEnableAdaptiveSampling\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// Compliance: retain ALL logs\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogging\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAddApplicationInsights\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogging\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetMinimumLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eDisabling adaptive sampling is non-negotiable for compliance. Sampling reduces costs by discarding a percentage of telemetry in high-volume scenarios, but it violates A.12.4.1\u0026rsquo;s requirement for complete audit trails. Auditors don\u0026rsquo;t accept \u0026ldquo;we probably captured most authentication attempts.\u0026rdquo; You need every event, or you need to implement server-side filtering based on severity and event type.\u003c/p\u003e\n\u003cp\u003eSetting the minimum log level to \u003ccode\u003eInformation\u003c/code\u003e balances detail with noise. You capture business events—orders created, payments processed, authentication decisions—while excluding the DEBUG and TRACE spam that provides zero audit value. For privileged operations, use \u003ccode\u003eLogWarning\u003c/code\u003e to ensure they stand out during review.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"audit-middleware\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#audit-middleware\" title=\"Audit Middleware\"\u003eAudit Middleware\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCapturing HTTP request/response cycles requires middleware that logs every inbound call with appropriate context. The pattern is straightforward but often implemented incorrectly:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e \u003cspan class=\"n\"\u003eInvokeAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpContext\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003escope\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eBeginScope\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eDictionary\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eobject\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e        [\u0026#34;CorrelationId\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eActivity\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCurrent\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eId\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"n\"\u003eGuid\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNewGuid\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e        [\u0026#34;UserId\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eIdentity\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e??\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;anonymous\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e        [\u0026#34;IPAddress\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConnection\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemoteIpAddress\u003c/span\u003e\u003cspan class=\"p\"\u003e?.\u003c/span\u003e\u003cspan class=\"n\"\u003eToString\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e        [\u0026#34;RequestPath\u0026#34;]\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;HTTP {Method} {Path} from {IPAddress}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMethod\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConnection\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRemoteIpAddress\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003e_next\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003elevel\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e400\u003c/span\u003e \u003cspan class=\"p\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWarning\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eLogLevel\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInformation\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elevel\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;HTTP {Method} {Path} completed with {StatusCode}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMethod\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ePath\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003econtext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eResponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStatusCode\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis middleware captures user identity from the authenticated context, IP address for geographic correlation, request path for access pattern analysis, and response status for success/failure tracking. Logging at different levels based on outcome—\u003ccode\u003eInformation\u003c/code\u003e for successful requests, \u003ccode\u003eWarning\u003c/code\u003e for 4xx client errors—enables alert rules that notify on elevated error rates without flooding dashboards with routine traffic.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"privileged-operations\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#privileged-operations\" title=\"Privileged Operations\"\u003ePrivileged Operations\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eA.12.4.3 demands enhanced logging for administrative actions. Identify endpoints that modify configuration, grant permissions, or access sensitive data, then tag them explicitly:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[Authorize(Roles = \u0026#34;Administrator\u0026#34;)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kd\"\u003easync\u003c/span\u003e \u003cspan class=\"n\"\u003eTask\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIActionResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eGrantRole\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eRoleRequest\u003c/span\u003e \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003e_logger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogWarning\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"s\"\u003e\u0026#34;PRIVILEGED: {AdminId} granting {Role} to {TargetId}\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFindFirstValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eClaimTypes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eNameIdentifier\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRoleName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// ...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eUsing \u003ccode\u003eLogWarning\u003c/code\u003e for privileged operations ensures they appear in filtered views even when successful. The \u0026ldquo;PRIVILEGED\u0026rdquo; prefix enables trivial querying: \u003ccode\u003etraces | where message startswith \u0026quot;PRIVILEGED\u0026quot;\u003c/code\u003e retrieves every admin action across your entire infrastructure. When an incident investigation requires understanding what privileges were modified leading up to a breach, you have that answer in seconds.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"cicd-validation\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#cicd-validation\" title=\"CI/CD Validation\"\u003eCI/CD Validation\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eCatch logging regressions before production:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eValidate no Console.WriteLine\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    if grep -r \u0026#34;Console\\.WriteLine\u0026#34; src/ --include=\u0026#34;*.cs\u0026#34; --exclude-dir=Tests; then\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e      echo \u0026#34;ERROR: Use ILogger, not Console.WriteLine\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e      exit 1\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    fi\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e- \u003cspan class=\"nt\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"l\"\u003eCheck for credential logging\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"nt\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e|\u003c/span\u003e\u003cspan class=\"sd\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    if grep -ri \u0026#34;password\\|apikey\\|secret\u0026#34; src/*.cs | grep -i \u0026#34;log\u0026#34;; then\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e      echo \u0026#34;WARNING: Review for sensitive data in logs\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"sd\"\u003e    fi\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"retention-and-access\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#retention-and-access\" title=\"Retention and Access\"\u003eRetention and Access\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eApplication Insights stores telemetry in Azure Log Analytics workspaces, which provide built-in retention and access controls that satisfy A.12.4.2. Configure retention to match your compliance requirements—typically 90 days minimum, often 365 days for regulated industries. Navigate to your Log Analytics workspace, find Usage and estimated costs, and set Data Retention appropriately.\u003c/p\u003e\n\u003cp\u003eRole-based access control restricts who can query logs. Grant read access to security teams using Azure\u0026rsquo;s \u0026ldquo;Log Analytics Reader\u0026rdquo; role. Developers troubleshooting production issues may need temporary access; implement time-bound role assignments via Azure Privileged Identity Management. The critical principle: developers who deploy code should not have unrestricted access to production logs containing user activity and authentication events.\u003c/p\u003e\n\u003cp\u003eApplication Insights\u0026rsquo; append-only storage model inherently satisfies immutability requirements. Once telemetry is ingested, it cannot be modified or deleted by application service principals. Only Azure administrators with workspace-level permissions can purge data, and those actions create audit logs in Azure Activity Log. Compromised application credentials cannot erase evidence.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"compliance-queries-kql\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#compliance-queries-kql\" title=\"Compliance Queries (KQL)\"\u003eCompliance Queries (KQL)\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAuditor evidence in seconds:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// All auth attempts for user 42\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003etraces\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"k\"\u003ewhere\u003c/span\u003e \u003cspan class=\"n\"\u003ecustomDimensions\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eUserId\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;42\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e       \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"k\"\u003ewhere\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e \u003cspan class=\"n\"\u003econtains\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;login\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Privileged operations\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003etraces\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"k\"\u003ewhere\u003c/span\u003e \u003cspan class=\"n\"\u003emessage\u003c/span\u003e \u003cspan class=\"n\"\u003estartswith\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;PRIVILEGED\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Failed requests over time\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003erequests\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"k\"\u003ewhere\u003c/span\u003e \u003cspan class=\"n\"\u003eresultCode\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"m\"\u003e400\u003c/span\u003e \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"n\"\u003esummarize\u003c/span\u003e \u003cspan class=\"n\"\u003ecount\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"k\"\u003eby\u003c/span\u003e \u003cspan class=\"n\"\u003ebin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etimestamp\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"n\"\u003eh\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eExport to CSV. Done.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"beyond-compliance\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#beyond-compliance\" title=\"Beyond Compliance\"\u003eBeyond Compliance\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTeams that implement structured logging for compliance invariably discover benefits that extend far beyond satisfying auditors.\u003c/p\u003e\n\u003cp\u003eWhen production breaks at 2 AM, correlation IDs trace requests across distributed services in seconds. No manual log aggregation, no timezone conversion, no guessing which error corresponds to which user session. Application Insights distributed tracing visualizes the failure path, and you fix the root cause instead of treating symptoms.\u003c/p\u003e\n\u003cp\u003eApplication Insights Smart Detection analyzes telemetry patterns and alerts on deviations—sudden increases in authentication failures that might indicate credential stuffing, spikes in 500 errors following deployments, elevated response times suggesting database contention. These signals emerge from structured logs; you can\u0026rsquo;t detect anomalies in unstructured text.\u003c/p\u003e\n\u003cp\u003eOne team I worked with discovered that 40% of their Azure SQL DTU consumption came from a single inefficient query—identified via structured logging of database operation timings. The logging infrastructure they built for compliance paid for itself in reduced cloud spend within three months.\u003c/p\u003e\n\u003cp\u003eAnd beyond ISO 27001, the same structured audit logs satisfy requirements from GDPR Article 30, SOC 2 CC7.2, and PCI-DSS Requirement 10. Build the infrastructure once, satisfy multiple regulatory frameworks.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eBuild observability that happens to satisfy compliance—not compliance theater that happens to generate logs.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"getting-started\"\u003e\u003ca href=\"/posts/audit-logging-azure-app-insights/#getting-started\" title=\"Getting Started\"\u003eGetting Started\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIf your current logging consists of \u003ccode\u003eConsole.WriteLine\u003c/code\u003e scattered throughout controllers, the path forward is straightforward. Add the \u003ccode\u003eMicrosoft.ApplicationInsights.AspNetCore\u003c/code\u003e NuGet package and configure the connection string. Replace string interpolation with message templates—convert \u003ccode\u003e$\u0026quot;User {userId}\u0026quot;\u003c/code\u003e to \u003ccode\u003e\u0026quot;User {UserId}\u0026quot;, userId\u003c/code\u003e so properties become queryable.\u003c/p\u003e\n\u003cp\u003eImplement correlation IDs via \u003ccode\u003eActivity.Current\u003c/code\u003e in request scopes, ensuring every log statement within a request shares the same identifier. Grep your codebase for \u003ccode\u003epassword\u003c/code\u003e, \u003ccode\u003eapikey\u003c/code\u003e, \u003ccode\u003esecret\u003c/code\u003e, and \u003ccode\u003etoken\u003c/code\u003e in logging statements. If you find matches, you have work to do before your next audit.\u003c/p\u003e\n\u003cp\u003eAdd audit middleware for HTTP request/response cycles. Configure Log Analytics retention appropriate to your industry. Verify that application service principals cannot delete telemetry data. Write the KQL queries your auditors will request and test them with your security team before the audit arrives.\u003c/p\u003e\n\u003cp\u003eA.12.4 doesn\u0026rsquo;t require expensive SIEMs or complex third-party compliance tools. It requires disciplined engineering: structured properties, protected storage, correlation across distributed systems, and consistent UTC timestamps. .NET\u0026rsquo;s \u003ccode\u003eILogger\u003c/code\u003e and Azure Application Insights provide these capabilities natively. The effort lies not in adopting new tools, but in applying existing tools correctly.\u003c/p\u003e\n\u003cp\u003eYour auditor checks a box. Your on-call engineers thank you at 2 AM. Your security team has visibility that actually matters when incidents occur.\u003c/p\u003e\n\u003cp\u003eThat\u0026rsquo;s compliance done right.\u003c/p\u003e\n","date_modified":"2026-05-26T10:22:03+02:00","date_published":"2026-01-29T17:00:00+01:00","id":"https://daily-devops.net/posts/audit-logging-azure-app-insights/","language":"en","summary":"Most audit logs fail when incidents happen. Structured logging with Application Insights creates trails auditors accept and engineers actually use.","tags":["iso-standards","logging","observability","azure","dotnet","security","compliance","bestpractices"],"title":"Audit Logging That Survives Your Next Security Incident","url":"https://daily-devops.net/posts/audit-logging-azure-app-insights/"}],"language":"en","title":"Structured Logging \u0026 Observability on Daily DevOps \u0026 .NET","version":"https://jsonfeed.org/version/1.1"}