Hello, I'm an open source software engineer in my late 30s living in #Seoul, #Korea, and an avid advocate of #FLOSS and the #fediverse.
I'm the creator of @fedify, an #ActivityPub server framework in #TypeScript, @hollo, an ActivityPub-enabled microblogging software for single users, and @botkit, a simple ActivityPub bot framework.
One of the enduring challenges in software programming is this: “How do we pass the invisible?” Loggers, HTTP request contexts, current locales, I/O handles—these pieces of information are needed throughout our programs, yet threading them explicitly through every function parameter would be unbearably verbose.
Throughout history, various approaches have emerged to tackle this problem. Dynamic scoping, aspect-oriented programming, context variables, and the latest effect systems… Some represent evolutionary steps in a continuous progression, while others arose independently. Yet we can view all these concepts through a unified lens.
Dynamic scoping
Dynamic scoping, which originated in 1960s Lisp, offered the purest form of solution. “A variable's value is determined not by where it's defined, but by where it's called.” Simple and powerful, yet it fell out of favor in mainstream programming languages after Common Lisp and Perl due to its unpredictability. Though we can still trace its lineage in JavaScript's this binding.
;; Common Lisp example - logger bound dynamically(defvar *logger* nil)(defun log-message (message) (when *logger* (funcall *logger* message)))(defun process-user-data (data) (log-message (format nil "Processing user: ~a" data)) ;; actual processing logic… )(defun main () (let ((*logger* (lambda (msg) (format t "[INFO] ~a~%" msg)))) (process-user-data "john@example.com"))) ; logger passed implicitly
Aspect-oriented programming
AOP structured the core idea of “modularizing cross-cutting concerns.” The philosophy: “Inject context, but with rules.” By separating cross-cutting concerns like logging and transactions into aspects, it maintained dynamic scoping's flexibility while pursuing more predictable behavior. However, debugging difficulties and performance overhead limited its spread beyond Java and .NET ecosystems.
// Spring AOP example - logging separated as cross-cutting concern@Aspectpublic class LoggingAspect { private Logger logger = LoggerFactory.getLogger(LoggingAspect.class); @Around("@annotation(Loggable)") public Object logMethodCall(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().getName(); logger.info("Entering method: " + methodName); Object result = joinPoint.proceed(); logger.info("Exiting method: " + methodName); return result; }}@Servicepublic class UserService { @Loggable // logger implicitly injected through aspect public User processUser(String userData) { // actual processing logic… return new User(userData); }}
Context variables
Context variables represent dynamic scoping redesigned for modern requirements—asynchronous and parallel programming. Python's contextvars and Java's ThreadLocal exemplify this approach. Yet they still suffer from runtime dependency and the fact that API context requirements are only discoverable through documentation.
Another manifestation of context variables appears in React's contexts and similar concepts in other UI frameworks. While their usage varies, they all solve the same problem: prop drilling. Implicit propagation through component trees mirrors propagation through function call stacks.
# Python contextvars example - custom logger propagated through contextfrom contextvars import ContextVar# Define custom logger function as context variablelogger_func = ContextVar('logger_func')def log_info(message): log_fn = logger_func.get() if log_fn: log_fn(f"[INFO] {message}")def process_user_data(data): log_info(f"Processing user: {data}") validate_user_data(data)def validate_user_data(data): log_info(f"Validating user: {data}") # logger implicitly propagateddef main(): # Set specific logger function in context def my_logger(msg): print(f"CustomLogger: {msg}") logger_func.set(my_logger) process_user_data("john@example.com")
Monads
Monads approach this from a different starting point. Rather than implicit context passing, monads attempt to encode effects in the type system—addressing a more fundamental problem. The Reader monad specifically corresponds to context variables. However, when combining multiple effects through monad transformers, complexity exploded. Developers had to wrestle with unwieldy types like ReaderT Config (StateT AppState (ExceptT Error IO)). Layer ordering mattered, each layer required explicit lifting, and usability suffered. Consequently, monadic ideas remained largely confined to serious functional programming languages like Haskell, Scala, and F#.
-- Haskell Logger monad example - custom Logger monad definitionnewtype Logger a = Logger (IO a)instance Functor Logger where fmap f (Logger io) = Logger (fmap f io)instance Applicative Logger where pure = Logger . pure Logger f <*> Logger x = Logger (f <*> x)instance Monad Logger where Logger io >>= f = Logger $ do a <- io let Logger io' = f a io'-- Logging functionslogInfo :: String -> Logger ()logInfo msg = Logger $ putStrLn $ "[INFO] " ++ msgprocessUserData :: String -> Logger ()processUserData userData = do logInfo $ "Processing user: " ++ userData validateUserData userDatavalidateUserData :: String -> Logger ()validateUserData userData = do logInfo $ "Validating user: " ++ userData -- logger passed through monadrunLogger :: Logger a -> IO arunLogger (Logger io) = iomain :: IO ()main = runLogger $ processUserData "john@example.com"
Effect systems
Effect systems emerged to solve the compositional complexity of monads. Implemented in languages like Koka and Eff, they operate through algebraic effects and handlers. Multiple effect layers compose without ordering constraints. Multiple overlapping layers require no explicit lifting. Effect handlers aren't fixed—they can be dynamically replaced, offering significant flexibility.
However, compiler optimizations remain immature, interoperability with existing ecosystems poses challenges, and the complexity of effect inference and its impact on type systems present ongoing research questions. Effect systems represent the newest approach discussed here, and their limitations will be explored as they gain wider adoption.
// Koka effect system example - logging effects flexibly propagatedeffect logger fun log-info(message: string): () fun log-error(message: string): ()fun process-user-data(user-data: string): logger () log-info("Processing user: " ++ user-data) validate-user-data(user-data)fun validate-user-data(user-data: string): logger () log-info("Validating user: " ++ user-data) // logger effect implicitly propagated if user-data == "" then log-error("Invalid user data: empty string")fun main() // Different logger implementations can be chosen dynamically with handler fun log-info(msg) println("[INFO] " ++ msg) fun log-error(msg) println("[ERROR] " ++ msg) process-user-data("john@example.com")
The art of passing the invisible—this is the essence shared by all the concepts discussed here, and it will continue to evolve in new forms as an eternal theme in software programming.
Requiring CW for all politics only works for people who have life that is not marked as "political"
ALT text detailsGamers are still convinced that there are only:
Two races: white and "political"
Two genders: Male and "political"
Two hair styles for women: long and "political"
Two sexualities: straight and "political"
Two body types: normative and "political"
On a related note, if you happen to know anyone who'd be interested in helping translate https://jointhefediverse.net to these languages, definitely feel free to pass this along!
Trying to build a cross-runtime test suite that works on Node.js, Bun, and Deno, but hitting a roadblock with Bun's incomplete node:test implementation. Missing subtests/test steps support is making this harder than it should be.
Just shared my thoughts on #JavaScript library #logging on Hacker News. Explores the fragmentation problem and dependency dilemmas from a library author's perspective. Would love to hear feedback from the #winston/#Pino users.
Building a JavaScript library is a delicate balance. You want to provide useful functionality while being respectful of your users' choices and constraints. When it comes to logging—something many libraries need for debugging, monitoring, and user support—this balance becomes particularly challenging.
The JavaScript ecosystem has evolved various approaches to this challenge, each with its own trade-offs. LogTape offers a different path, one that's specifically designed with library authors in mind.
The current state of library logging
If you've built libraries before, you've probably encountered the logging dilemma. Your library would benefit from logging—perhaps to help users debug integration issues, trace internal state changes, or provide insights into performance bottlenecks. But how do you add this capability responsibly?
Currently, popular libraries handle this challenge in several ways:
The debug approach
Libraries like Express and Socket.IO use the lightweight debug package, which allows users to enable logging through environment variables (DEBUG=express:*). This works well but creates a separate logging system that doesn't integrate with users' existing logging infrastructure.
Custom logging systems
Libraries like Mongoose and Prisma have built their own logging mechanisms. Mongoose offers mongoose.set('debug', true) while Prisma uses its own logging configuration. These approaches work, but each library creates its own logging API that users must learn separately.
Application-focused libraries
winston, Pino, and Bunyan are powerful logging solutions, but they're primarily designed for applications rather than libraries. Using them in a library means imposing significant dependencies and potentially conflicting with users' existing logging choices.
No logging at all
Many library authors avoid the complexity entirely, leaving their libraries silent and making debugging more challenging for everyone involved.
Dependency injection
Some libraries adopt a more sophisticated approach by accepting a logger instance from the application through their configuration or constructor parameters. This maintains clean separation of concerns and allows libraries to use whatever logging system the application has chosen. However, this pattern requires more complex APIs and places additional burden on library users to understand and configure logging dependencies.
Each approach represents a reasonable solution to a genuine problem, but none fully addresses the core tension: how do you provide valuable diagnostic capabilities without imposing choices on your users?
The fragmentation problem
There's another challenge that emerges when libraries each solve logging in their own way: fragmentation. Consider a typical Node.js application that might use Express for the web framework, Socket.IO for real-time communication, Axios for HTTP requests, Mongoose for database access, and several other specialized libraries.
Each library potentially has its own logging approach:
Authentication libraries often include their own logging mechanisms
From an application developer's perspective, this creates a management challenge. They must learn and configure multiple different logging systems, each with its own syntax, capabilities, and quirks. Logs are scattered across different outputs with inconsistent formats, making it difficult to get a unified view of what's happening in their application.
The lack of integration also means that powerful features like structured logging, log correlation, and centralized log management become much harder to implement consistently across all the libraries in use.
LogTape's approach
LogTape attempts to address these challenges with what might be called a “library-first design.” The core principle is simple but potentially powerful: if logging isn't configured, nothing happens. No output, no errors, no side effects—just complete transparency.
This approach allows you to add comprehensive logging to your library without any impact on users who don't want it. When a user imports your library and runs their code, LogTape's logging calls are essentially no-ops until someone explicitly configures logging. Users who want insights into your library's behavior can opt in; those who don't are completely unaffected.
More importantly, when users do choose to configure logging, all LogTape-enabled libraries can be managed through a single, unified configuration system. This means one consistent API, one log format, and one destination for all library logs while still allowing fine-grained control over what gets logged from which libraries.
Note
This approach isn't entirely novel—it draws inspiration from Python's standard logging library, which has successfully created a unified logging ecosystem. In Python, libraries like Requests, SQLAlchemy, and Django components all use the standard logging framework, allowing developers to configure all library logging through a single, consistent system. This has proven to be both practical and powerful, enabling rich diagnostic capabilities across the entire Python ecosystem while maintaining simplicity for application developers.
// In your library code - completely safe to includeimport { getLogger } from "@logtape/logtape";const logger = getLogger(["my-awesome-lib", "database"]);export function connectToDatabase(config) { logger.debug("Attempting database connection", { config }); // ... your logic logger.info("Database connection established");}
The dependency consideration
Modern JavaScript development involves careful consideration of dependencies. While popular logging libraries like winston and Pino are well-maintained and widely trusted, they do come with their own dependency trees. winston, for example, includes 17 dependencies, while Pino includes 1.
For library authors, this creates a consideration: every dependency you add becomes a dependency for your users, whether they want it or not. This isn't necessarily problematic (many excellent libraries have dependencies), but it does represent a choice you're making on behalf of your users.
LogTape takes a different approach with zero dependencies. This isn't just a philosophical choice—it has practical implications for your library's users. They won't see additional packages in their node_modules, won't need to worry about supply chain considerations for logging-related dependencies, and won't face potential version conflicts between your logging choice and theirs.
At just 5.3KB minified and gzipped, LogTape adds minimal weight to their bundles. The installation process becomes faster, the dependency tree stays cleaner, and security audits remain focused on the dependencies that directly serve your library's core functionality.
Breaking the compatibility chain
Here's a challenge that might be familiar: you want your library to support both ESM and CommonJS environments. Perhaps some of your users are working with legacy Node.js projects that rely on CommonJS, while others are using modern ESM setups or building for browsers.
The challenge becomes apparent when you have dependencies. While ESM modules can import CommonJS modules without issues, the reverse isn't true—CommonJS modules cannot require ESM-only packages (at least not until the experimental features in Node.js 22+ become stable). This creates an asymmetric compatibility constraint.
If your library depends on any ESM-only packages, your library effectively becomes ESM-only as well, since CommonJS environments won't be able to use it. This means that even one ESM-only dependency in your chain can prevent you from supporting CommonJS users.
LogTape supports both ESM and CommonJS completely, meaning it won't be the weak link that forces this limitation. Whether your users are working with legacy Node.js projects, cutting-edge ESM applications, or hybrid environments, LogTape adapts seamlessly to their setup.
More importantly, when LogTape provides native ESM support (rather than just being importable as CommonJS), it enables tree shaking in modern bundlers. Tree shaking allows bundlers to eliminate unused code during the build process, but it requires the static import/export structure that only ESM provides. While CommonJS modules can be imported into ESM projects, they're often treated as opaque blocks that can't be optimized, potentially including unused code in the final bundle.
For a logging library that aims to have minimal impact, this optimization capability can be meaningful, especially for applications where bundle size matters.
Universal runtime support
The JavaScript ecosystem spans an impressive range of runtime environments today. Your library might run in Node.js servers, Deno scripts, Bun applications, web browsers, or edge functions. LogTape works identically across all of these environments without requiring polyfills, compatibility layers, or runtime-specific code.
This universality means you can focus on your library's core functionality rather than worrying about whether your logging choice will work in every environment your users might encounter. Whether someone imports your library into a Cloudflare Worker, a Next.js application, or a Deno CLI tool, the logging behavior remains consistent and reliable.
Performance without compromise
One concern library authors often have about logging is performance impact. What if your users import your library into a high-performance application? What if they're running in a memory-constrained environment?
LogTape addresses this with remarkable efficiency when logging is disabled. The overhead of an unconfigured LogTape call is virtually zero—among the lowest of any logging solution available. This means you can add detailed logging throughout your library for development and debugging purposes without worrying about performance impact on users who don't enable it.
When logging is enabled, LogTape consistently outperforms other libraries, particularly for console output—often the most common logging destination during development.
Avoiding namespace collisions
Libraries sharing the same application can create logging chaos when they all output to the same namespace. LogTape's hierarchical category system elegantly solves this by encouraging libraries to use their own namespaces.
Your library might use categories like ["my-awesome-lib", "database"] or ["my-awesome-lib", "validation"], ensuring that your logs are clearly separated from other libraries and the main application. Users who configure LogTape can then control logging levels independently for different libraries and different components within those libraries.
Developer experience that just works
LogTape is built with TypeScript from the ground up, meaning your TypeScript-based library gets full type safety without additional dependencies or type packages. The API feels natural and modern, supporting both template literals and structured logging patterns that integrate well with contemporary JavaScript development practices.
// Template literal style - feels naturallogger.info`User ${userId} performed action ${action}`;// Structured logging - great for monitoringlogger.info("User action completed", { userId, action, duration });
Practical integration
Actually using LogTape in your library is refreshingly straightforward. You simply import the logger, create appropriately namespaced categories, and log where it makes sense. No configuration, no setup, no complex initialization sequences.
import { getLogger } from "@logtape/logtape";const logger = getLogger(["my-lib", "api"]);export async function fetchUserData(userId) { logger.debug("Fetching user data", { userId }); try { const response = await api.get(`/users/${userId}`); logger.info("User data retrieved successfully", { userId, status: response.status }); return response.data; } catch (error) { logger.error("Failed to fetch user data", { userId, error }); throw error; }}
For users who want to see these logs, configuration is equally simple:
If your potential users are already invested in other logging systems, LogTape provides adapters for popular libraries like winston and Pino. This allows LogTape-enabled libraries to integrate with existing logging infrastructure, routing their logs through whatever system applications are already using.
The existence of these adapters reveals an honest truth: LogTape isn't yet a widely-adopted standard in the JavaScript ecosystem. Most applications are still built around established logging libraries, and asking users to completely restructure their logging approach would be unrealistic. The adapters represent a practical compromise—they allow library authors to take advantage of LogTape's library-friendly design while respecting users' existing investments and preferences.
This approach reduces friction for adoption while still providing library authors with a modern, zero-dependency logging API. Perhaps over time, as more libraries adopt this pattern and more developers experience its benefits, the need for such adapters might diminish. But for now, they serve as a pragmatic bridge between LogTape's vision and the current reality of the ecosystem.
A choice worth considering
Ultimately, choosing LogTape for your library represents a particular philosophy about the relationship between libraries and applications. It's about providing capabilities while preserving choice, offering insights while avoiding imposition.
The traditional approaches—whether using debug packages, application-focused loggers, or custom solutions—each have their merits and have served the community well. LogTape simply offers another option: one designed specifically for the unique position libraries occupy in the JavaScript ecosystem.
For library authors, this approach might offer several practical benefits. Your library gets detailed logging for development, debugging, and user support, while your users retain complete autonomy over whether and how to use those capabilities.
The broader benefit might be a more cohesive logging experience across the JavaScript ecosystem—one where libraries can provide rich diagnostic information that integrates seamlessly with whatever logging strategy applications choose to employ.
In a world where every dependency decision has implications, LogTape offers an approach worth considering: a way to enhance your library's capabilities while respecting your users' preferences and existing choices.
The secret to my productivity: I take every other day off, and when I do code, I get in the zone with music and allocate some time to respond to bug reports, check issues/discord/matrix, plan new features/work on new features and share recent progress.
I've been doing this for years, and while it may not work for everyone, it works for me.
FediDB was launched in 2019 and just got a fresh coat of paint in April.
Fediverse.info launched in 2021 and just this week was redesigned.
@dansup Your productivity is genuinely inspiring! Shipping 3 new sites while simultaneously refactoring and advancing multiple projects—that's incredible dedication. The fediverse ecosystem is fortunate to have someone so committed to moving things forward.