{
  "version": "https://jsonfeed.org/version/1",
  "title": "Opensource on LLBBL Blog",
  "icon": "https://avatars.micro.blog/avatars/2023/40/125738.jpg",
  "home_page_url": "https://llbbl.blog/",
  "feed_url": "https://llbbl.blog/feed.json",
  "items": [
      {
        "id": "http://llbbl.micro.blog/2026/09/05/my-logger-documented-four-environment.html",
        "title": "My Logger Documented Four Environment Variables. None of Them Worked.",
        "content_html": "<p>I shipped a logging library with a <code>LOG_LEVEL</code> environment variable. The README documented it. A dedicated docs page documented it with examples and runtime-specific caveats. There were unit tests covering it, and they passed on every CI run for just under thirteen months.</p>\n<p>Nothing called the function.</p>\n<p>Here&rsquo;s the command that found it. One line, run against the last 1.x tag:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"display:flex;\"><span>$ git grep loadConfigFromEnvironment v1.1.21 -- src/\n</span></span><span style=\"display:flex;\"><span>v1.1.21:src/utils/config.ts:22:export <span style=\"color:#66d9ef\">function</span> loadConfigFromEnvironment<span style=\"color:#f92672\">()</span>: Partial&lt;LoggerConfig&gt; <span style=\"color:#f92672\">{</span>\n</span></span></code></pre></div><p>That&rsquo;s the whole result. One hit, and it&rsquo;s the definition of the function itself. Exported, documented, tested, and reachable by exactly nobody.</p>\n<h2 id=\"the-function-was-fine\">The function was fine</h2>\n<p>That&rsquo;s the part that took me a minute to accept. There was no bug in it. Given a <code>process.env</code>, it did the right thing:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">export</span> <span style=\"color:#66d9ef\">function</span> <span style=\"color:#a6e22e\">loadConfigFromEnvironment</span>()<span style=\"color:#f92672\">:</span> <span style=\"color:#a6e22e\">Partial</span>&lt;<span style=\"color:#f92672\">LoggerConfig</span>&gt; {\n</span></span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">config</span>: <span style=\"color:#66d9ef\">Partial</span>&lt;<span style=\"color:#f92672\">LoggerConfig</span>&gt; <span style=\"color:#f92672\">=</span> {};\n</span></span><span style=\"display:flex;\"><span>\n</span></span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">if</span> (<span style=\"color:#66d9ef\">typeof</span> <span style=\"color:#a6e22e\">process</span> <span style=\"color:#f92672\">!==</span> <span style=\"color:#e6db74\">&#39;undefined&#39;</span> <span style=\"color:#f92672\">&amp;&amp;</span> <span style=\"color:#a6e22e\">process</span>.<span style=\"color:#a6e22e\">env</span>) {\n</span></span><span style=\"display:flex;\"><span>    <span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">env</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">process</span>.<span style=\"color:#a6e22e\">env</span>;\n</span></span><span style=\"display:flex;\"><span>\n</span></span><span style=\"display:flex;\"><span>    <span style=\"color:#66d9ef\">if</span> (<span style=\"color:#a6e22e\">env</span>.<span style=\"color:#a6e22e\">LOG_LEVEL</span>) {\n</span></span><span style=\"display:flex;\"><span>      <span style=\"color:#a6e22e\">config</span>.<span style=\"color:#a6e22e\">level</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">stringToLogLevel</span>(<span style=\"color:#a6e22e\">env</span>.<span style=\"color:#a6e22e\">LOG_LEVEL</span>);\n</span></span><span style=\"display:flex;\"><span>    }\n</span></span><span style=\"display:flex;\"><span>    <span style=\"color:#75715e\">// ...LOG_FORMAT, LOG_TIMESTAMP, LOG_COLOR\n</span></span></span><span style=\"display:flex;\"><span>  }\n</span></span><span style=\"display:flex;\"><span>\n</span></span><span style=\"display:flex;\"><span>  <span style=\"color:#66d9ef\">return</span> <span style=\"color:#a6e22e\">config</span>;\n</span></span><span style=\"display:flex;\"><span>}\n</span></span></code></pre></div><p>Clean. Reasonable. Correct. And <code>createLogger()</code> never called it, so a user could set <code>LOG_LEVEL=debug</code>, restart their process, and get exactly the same output they got before.</p>\n<p>The tests found this function the same way a unit test finds anything: by importing it directly. <code>tests/config.test.ts</code> referenced <code>loadConfigFromEnvironment</code> <strong>eleven times</strong> across eight assertions. Every one of them passed. Not a single one of them proved a user could reach it, because none of them went through the public entry point.</p>\n<p>That&rsquo;s the failure mode. A unit test that imports the unit is testing the unit. It&rsquo;s not testing whether the unit is <em>wired up</em>.</p>\n<h2 id=\"it-wasnt-the-only-one\">It wasn&rsquo;t the only one</h2>\n<p>While I was in there I checked <code>LoggerConfig.transports</code>, declared right in the public type at <code>src/core/types.ts:70</code>:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"display:flex;\"><span><span style=\"color:#a6e22e\">transports?</span>: <span style=\"color:#66d9ef\">TransportConfig</span>[];\n</span></span></code></pre></div><p>TypeScript happily accepted a transports array. Autocomplete offered it. And <code>NodeLogger</code> ignored it completely, because the Node adapter built its own hardcoded Winston transport list and never looked at the config field:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"display:flex;\"><span><span style=\"color:#66d9ef\">const</span> <span style=\"color:#a6e22e\">logger</span> <span style=\"color:#f92672\">=</span> <span style=\"color:#a6e22e\">winston</span>.<span style=\"color:#a6e22e\">createLogger</span>({\n</span></span><span style=\"display:flex;\"><span>  <span style=\"color:#a6e22e\">transports</span><span style=\"color:#f92672\">:</span> [\n</span></span><span style=\"display:flex;\"><span>    <span style=\"color:#66d9ef\">new</span> <span style=\"color:#a6e22e\">winston</span>.<span style=\"color:#a6e22e\">transports</span>.<span style=\"color:#a6e22e\">Console</span>({ <span style=\"color:#75715e\">/* ... */</span> }),\n</span></span><span style=\"display:flex;\"><span>  ],\n</span></span><span style=\"display:flex;\"><span>});\n</span></span></code></pre></div><p>So you could pass <code>transports: [{ type: 'file', options: { filename: 'app.log' } }]</code>, get no type error, get no runtime warning, and get no file. The type system confirmed your configuration was valid. The runtime threw it away.</p>\n<p>Two different features, same shape. Declared in the public surface, absent from the code path that runs.</p>\n<h2 id=\"what-the-fix-actually-cost\">What the fix actually cost</h2>\n<p>I wired it up in 2.0.0 and the breaking-change note is longer than most of the feature work:</p>\n<blockquote>\n<p>LOG_LEVEL, LOG_FORMAT, LOG_TIMESTAMP and LOG_COLOR now take effect, so a process with any of them already set logs differently after upgrading without any code change, and they override configuration passed to createLogger().</p>\n</blockquote>\n<p>Read that carefully. <strong>Turning on a documented feature is a breaking change</strong> when the feature has been off long enough. Somebody out there has <code>LOG_LEVEL=debug</code> sitting in a <code>.env</code> from a project they set up last year, inherited by a service that has been logging at <code>info</code> this whole time because the variable did nothing. They upgrade a minor version, and now their logs are ten times bigger.</p>\n<p>There&rsquo;s a nastier one buried in the same note. The old boolean parsing was <code>env.LOG_TIMESTAMP.toLowerCase() === 'true'</code>, so <code>LOG_TIMESTAMP=1</code> evaluated to <strong>false</strong>. Anybody who wrote <code>1</code> and expected <code>true</code> was wrong twice over, first because the parse was strict about the literal string, and second because nothing read the result anyway. The new parser accepts <code>true, 1, yes, on</code> and <code>false, 0, no, off</code>, which means <code>LOG_TIMESTAMP=1</code> flips from false to true across the upgrade.</p>\n<p>Nobody could have hit that in practice. The feature was dead. But the moment it goes live, every one of those latent misconfigurations wakes up at once.</p>\n<h2 id=\"how-this-happens\">How this happens</h2>\n<p>Four things worth stealing from this:</p>\n<ol>\n<li><strong>Grep your own exports for call sites.</strong> <code>git grep &lt;exportName&gt; -- src/</code> on any function you believe is load-bearing. If the only hit is the definition, you found one.</li>\n<li><strong>A unit test proves a unit works, not that it runs.</strong> At least one test per feature should enter through the same door a user does.</li>\n<li><strong>A declared field on a public type is a promise.</strong> If the runtime ignores it, the type system is lying to your users with full IDE autocomplete support.</li>\n<li><strong>Turning on a dead documented feature is a breaking change.</strong> Version it like one, and write the note that tells people their existing environment now means something.</li>\n</ol>\n<p>The library is at 2.5.2 now and the environment variables work. That sentence should not have taken thirteen months to become true.</p>\n<h2 id=\"sources\">Sources</h2>\n<ul>\n<li><a href=\"https://github.com/llbbl/logan-logger-ts\">logan-logger-ts</a> — the repo, if you want the diffs</li>\n<li><a href=\"https://github.com/llbbl/logan-logger-ts/commit/bc398b0\">The 2.0.0 commit</a> — where the wiring finally happened, breaking-change note and all</li>\n<li><a href=\"https://github.com/llbbl/treering\">treering</a> — the language-neutral spec and conformance fixtures that drive the library through its public API</li>\n<li><a href=\"https://12factor.net/config\">The Twelve-Factor App: Config</a> — the case for environment-based configuration, which works better when you read the environment</li>\n</ul>\n<blockquote>\n<p>I&rsquo;d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at <a href=\"https://micro.blog/llbbl?remote_follow=1\">@logan@llbbl.blog</a>.</p>\n</blockquote>\n",
        "date_published": "2026-09-05T10:00:00-05:00",
        "url": "https://llbbl.blog/2026/09/05/my-logger-documented-four-environment.html",
        "tags": ["Testing","Typescript","Configuration","Opensource"]
      }
  ]
}
