<rss version="2.0">
  <channel>
    <title>Opensource on LLBBL Blog</title>
    <link>https://llbbl.blog/categories/opensource/</link>
    <description></description>
    
    <language>en</language>
    
    <lastBuildDate>Sat, 05 Sep 2026 10:00:00 -0500</lastBuildDate>
    
    <item>
      <title>My Logger Documented Four Environment Variables. None of Them Worked.</title>
      <link>https://llbbl.blog/2026/09/05/my-logger-documented-four-environment.html</link>
      <pubDate>Sat, 05 Sep 2026 10:00:00 -0500</pubDate>
      
      <guid>http://llbbl.micro.blog/2026/09/05/my-logger-documented-four-environment.html</guid>
      <description>&lt;p&gt;I shipped a logging library with a &lt;code&gt;LOG_LEVEL&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;Nothing called the function.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the command that found it. One line, run against the last 1.x tag:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ git grep loadConfigFromEnvironment v1.1.21 -- src/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;v1.1.21:src/utils/config.ts:22:export &lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; loadConfigFromEnvironment&lt;span style=&#34;color:#f92672&#34;&gt;()&lt;/span&gt;: Partial&amp;lt;LoggerConfig&amp;gt; &lt;span style=&#34;color:#f92672&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s the whole result. One hit, and it&amp;rsquo;s the definition of the function itself. Exported, documented, tested, and reachable by exactly nobody.&lt;/p&gt;
&lt;h2 id=&#34;the-function-was-fine&#34;&gt;The function was fine&lt;/h2&gt;
&lt;p&gt;That&amp;rsquo;s the part that took me a minute to accept. There was no bug in it. Given a &lt;code&gt;process.env&lt;/code&gt;, it did the right thing:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;loadConfigFromEnvironment&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Partial&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;LoggerConfig&lt;/span&gt;&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;config&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;Partial&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;LoggerConfig&lt;/span&gt;&amp;gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;typeof&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;process&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;undefined&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;process&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;env&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;env&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;process&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;env&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;env&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;LOG_LEVEL&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;config&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;level&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;stringToLogLevel&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;env&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;LOG_LEVEL&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// ...LOG_FORMAT, LOG_TIMESTAMP, LOG_COLOR
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;config&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Clean. Reasonable. Correct. And &lt;code&gt;createLogger()&lt;/code&gt; never called it, so a user could set &lt;code&gt;LOG_LEVEL=debug&lt;/code&gt;, restart their process, and get exactly the same output they got before.&lt;/p&gt;
&lt;p&gt;The tests found this function the same way a unit test finds anything: by importing it directly. &lt;code&gt;tests/config.test.ts&lt;/code&gt; referenced &lt;code&gt;loadConfigFromEnvironment&lt;/code&gt; &lt;strong&gt;eleven times&lt;/strong&gt; 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.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the failure mode. A unit test that imports the unit is testing the unit. It&amp;rsquo;s not testing whether the unit is &lt;em&gt;wired up&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&#34;it-wasnt-the-only-one&#34;&gt;It wasn&amp;rsquo;t the only one&lt;/h2&gt;
&lt;p&gt;While I was in there I checked &lt;code&gt;LoggerConfig.transports&lt;/code&gt;, declared right in the public type at &lt;code&gt;src/core/types.ts:70&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;transports?&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;TransportConfig&lt;/span&gt;[];
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;TypeScript happily accepted a transports array. Autocomplete offered it. And &lt;code&gt;NodeLogger&lt;/code&gt; ignored it completely, because the Node adapter built its own hardcoded Winston transport list and never looked at the config field:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;logger&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;winston&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;createLogger&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;transports&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;winston&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;transports&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Console&lt;/span&gt;({ &lt;span style=&#34;color:#75715e&#34;&gt;/* ... */&lt;/span&gt; }),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So you could pass &lt;code&gt;transports: [{ type: &#39;file&#39;, options: { filename: &#39;app.log&#39; } }]&lt;/code&gt;, 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.&lt;/p&gt;
&lt;p&gt;Two different features, same shape. Declared in the public surface, absent from the code path that runs.&lt;/p&gt;
&lt;h2 id=&#34;what-the-fix-actually-cost&#34;&gt;What the fix actually cost&lt;/h2&gt;
&lt;p&gt;I wired it up in 2.0.0 and the breaking-change note is longer than most of the feature work:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;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().&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Read that carefully. &lt;strong&gt;Turning on a documented feature is a breaking change&lt;/strong&gt; when the feature has been off long enough. Somebody out there has &lt;code&gt;LOG_LEVEL=debug&lt;/code&gt; sitting in a &lt;code&gt;.env&lt;/code&gt; from a project they set up last year, inherited by a service that has been logging at &lt;code&gt;info&lt;/code&gt; this whole time because the variable did nothing. They upgrade a minor version, and now their logs are ten times bigger.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a nastier one buried in the same note. The old boolean parsing was &lt;code&gt;env.LOG_TIMESTAMP.toLowerCase() === &#39;true&#39;&lt;/code&gt;, so &lt;code&gt;LOG_TIMESTAMP=1&lt;/code&gt; evaluated to &lt;strong&gt;false&lt;/strong&gt;. Anybody who wrote &lt;code&gt;1&lt;/code&gt; and expected &lt;code&gt;true&lt;/code&gt; 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 &lt;code&gt;true, 1, yes, on&lt;/code&gt; and &lt;code&gt;false, 0, no, off&lt;/code&gt;, which means &lt;code&gt;LOG_TIMESTAMP=1&lt;/code&gt; flips from false to true across the upgrade.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2 id=&#34;how-this-happens&#34;&gt;How this happens&lt;/h2&gt;
&lt;p&gt;Four things worth stealing from this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Grep your own exports for call sites.&lt;/strong&gt; &lt;code&gt;git grep &amp;lt;exportName&amp;gt; -- src/&lt;/code&gt; on any function you believe is load-bearing. If the only hit is the definition, you found one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A unit test proves a unit works, not that it runs.&lt;/strong&gt; At least one test per feature should enter through the same door a user does.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A declared field on a public type is a promise.&lt;/strong&gt; If the runtime ignores it, the type system is lying to your users with full IDE autocomplete support.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Turning on a dead documented feature is a breaking change.&lt;/strong&gt; Version it like one, and write the note that tells people their existing environment now means something.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The library is at 2.5.2 now and the environment variables work. That sentence should not have taken thirteen months to become true.&lt;/p&gt;
&lt;h2 id=&#34;sources&#34;&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/llbbl/logan-logger-ts&#34;&gt;logan-logger-ts&lt;/a&gt; — the repo, if you want the diffs&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/llbbl/logan-logger-ts/commit/bc398b0&#34;&gt;The 2.0.0 commit&lt;/a&gt; — where the wiring finally happened, breaking-change note and all&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/llbbl/treering&#34;&gt;treering&lt;/a&gt; — the language-neutral spec and conformance fixtures that drive the library through its public API&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://12factor.net/config&#34;&gt;The Twelve-Factor App: Config&lt;/a&gt; — the case for environment-based configuration, which works better when you read the environment&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;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 &lt;a href=&#34;https://micro.blog/llbbl?remote_follow=1&#34;&gt;@logan@llbbl.blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
  </channel>
</rss>