Show HN: C++、Java 和 C# 轻量级日志记录器
Show HN: C++, Java and C# light-weight-logger

原始链接: https://github.com/PenguineDavid/light-weight-logger

**Light-Weight-Logger** 是一个零依赖、跨平台的终端日志库,支持 C++、C# 和 Java。该库基于 LGPL v2.1 协议发布,采用高度可定制的格式字符串驱动方式进行日志记录,而非依赖固定的日志级别。 **核心功能:** * **可定制输出:** 用户可在运行时注册日志级别,并为每个级别定义独特的 ANSI 颜色和格式字符串。 * **动态格式化:** 内置功能强大的微型语言,支持丰富的元数据,包括时间戳、线程 ID,以及通过 `%S` 说明符实现自动化的级别名称列对齐。 * **源码位置:** 可直接从调用栈获取文件名、行号和函数名。 * **易于使用:** C++ 版本提供仅含头文件的即插即用方案;C# 和 Java 版本仅需两个文件。无需集成构建系统或包管理器。 * **灵活性:** 日期格式(澳洲/美式)可在运行时切换,解析器通过单次遍历处理说明符,执行效率高。 该库专为简化开发与集成而设计,非常适合希望对终端输出进行细粒度控制且不愿增加外部依赖的开发者。日志调用线程安全,适用于并发应用程序。

对不起。
相关文章

原文

Format-string driven terminal logging library for C++, C# and Java.

Licensed under the GNU Lesser General Public License v2.1 | Source: github.com/PenguineDavid/light-weight-logger



Logger is a lightweight, zero-dependency terminal logging library. Instead of a fixed set of log levels with hard-coded output, it gives you a format-string mini-language. You register named levels at runtime, attach a format string and an ANSI colour to each, and call log() / Log() with a level name and message. The parser walks the format string one character at a time, expanding percent-encoded specifiers into live metadata at call time.

Key properties:

  • No third-party dependencies in any port
  • Single-file drop-in for C++ (Logger.hpp), two-file for C# and Java
  • ANSI colour output via the Colour constants shipped with each port
  • Dynamic %S padding keeps level names column-aligned automatically as you register more levels
  • Source-location capture (%F, %L, %f) available in all three ports
  • Date format switchable between AU (DD/MM/YYYY) and US (MM/DD/YYYY) at runtime

Format Specifier Reference

Every format string is a plain string in which any two-character sequence starting with % is treated as a specifier. All other characters are emitted verbatim. Specifiers are expanded left-to-right in a single pass at call time.

Specifier Description
%C Emit the ANSI colour code registered for this level. Use at the start of coloured regions.
%G Emit ANSI grey (\033[90m). Typically used for metadata/secondary information.
%c Emit ANSI reset (\033[0m). Required to close any colour region.
Specifier Description
%S Emit trailing spaces to align the level name column with the longest registered name. Recalculated automatically when new levels are added.
%% Literal percent sign.
Specifier Description
%N Level name (the string you passed to add_format / AddFormat / addFormat).
%M The log message passed to log() / Log().
%T Current wall-clock time: HH:MM:SS.
%D Current date: DD/MM/YYYY (AU) or MM/DD/YYYY (US). Controlled by change_date_format.
%ms Current milliseconds component (000-999). Three-character sequence: %, m, s.
%Z System timezone name or abbreviation (e.g. AEST, PST, UTC).
%t Current thread ID. std::this_thread::get_id() in C++, ManagedThreadId in C#, threadId() in Java.

Source Location Specifiers

These resolve at call time by inspecting the call stack. In C++ this uses std::source_location (C++20). In C# this uses System.Diagnostics.StackFrame. In Java this walks Thread.currentThread().getStackTrace().

Specifier Description
%F Source file of the call site.
%L Line number of the log() call.
%f Function or method name at the call site.

Note: %F, %L and %f add overhead on every log call because they walk the call stack. Omit them from format strings used on hot paths.


The three format strings below are demonstrated in the example files. They are ordinary strings -- copy, modify, or ignore them.

%C[%N]%c%S %G[%D %T %Z]%c -> %M
Screenshot 2026-06-15 193518

Style 2: Clean Pipe Minimalist

%C %N %c%S | %G%T %Z%c | %M
Screenshot 2026-06-15 193549

Style 3: Cloud Engine (with source metadata)

%G%D %T.%ms %Z%c %C<%N>%c%S %M %G(%F:%L)%c
Screenshot 2026-06-16 000859

Any combination of specifiers and literal text is valid:

// Compact -- just level and message, no timestamp
%C[%N]%c%S %M

// ISO-style with milliseconds
%G[%D %T.%ms]%c %C%N%c%S %M

// Thread-tagged for concurrent applications
%G[%T]%c [thread:%t] %C%N%c%S %M

// Full debug dump with source location
%C%N%c%S %M %G@ %F:%L in %f%c

Standard: C++20 | File: Logger.hpp | Namespace: Colour (ANSI constants)

Logger.hpp is header-only. Drop it anywhere on your include path and include it directly -- no build system integration required.

#include "Logger.hpp"

int main()
{
    Logger logger;

    std::string fmt = "%C[%N]%c%S %G[%D %T %Z]%c -> %M";
    logger.add_format("INFO",    fmt, Colour::CYAN);
    logger.add_format("SUCCESS", fmt, Colour::GREEN);
    logger.add_format("WARN",    fmt, Colour::YELLOW);
    logger.add_format("ERROR",   fmt, Colour::RED);

    logger.log("INFO",    "Initializing core subsystem components.");
    logger.log("SUCCESS", "Database connection established smoothly.");
    logger.log("WARN",    "High memory usage detected on node cluster.");
    logger.log("ERROR",   "Failed to write to write-ahead log!");
}
# GCC / Clang
g++ -std=c++20 -o app main.cpp

# MSYS2 ucrt64 (Windows)
g++ -std=c++20 -o app.exe main.cpp

void add_format(const std::string& name, const std::string& format_string, std::string_view level_colour = Colour::WHITE)

Register a named log level. Calling again with the same name replaces the format string and colour. max_name_length is updated so %S padding stays consistent.

Parameter Description
name Arbitrary string used as the level identifier. Passed back via %N.
format_string Format string using the specifiers above.
level_colour One of the Colour:: constants. Defaults to Colour::WHITE. Emitted by %C.

void log(const std::string& format_name, const std::string& message, const std::source_location location = std::source_location::current())

Emit a log line. If format_name does not match a registered level, outputs [UNREGISTERED] followed by the message. Do not pass location -- the compiler-supplied default captures the call site automatically.

Parameter Description
format_name Must match a name passed to add_format.
message Free-form string emitted by %M.
location Do not pass. Default captures the call site.

void change_date_format(std::string type)

Switch date formatting. Accepts "au" (DD/MM/YYYY, default) or "us" (MM/DD/YYYY). Any other value is silently ignored.

Colour::RESET    // \033[0m
Colour::BLACK    // \033[30m
Colour::RED      // \033[31m
Colour::GREEN    // \033[32m
Colour::YELLOW   // \033[33m
Colour::BLUE     // \033[34m
Colour::MAGENTA  // \033[35m
Colour::CYAN     // \033[36m
Colour::WHITE    // \033[37m
Colour::GREY     // \033[90m

All constants are constexpr std::string_view defined in namespace Colour.

All standard library:

Header Used for
<iostream>, <string>, <unordered_map> Core I/O and storage
<chrono>, <iomanip> Timestamp formatting
<source_location> C++20. Required for %F, %L, %f
<thread> %t (thread ID)
<algorithm> std::max for max_name_length

Runtime: .NET 8+ | Files: Logger.cs + Colour.cs | Namespace: include

Add both files to your project. No NuGet packages required.

using include;

Logger logger = new Logger();

string fmt = "%C[%N]%c%S %G[%D %T %Z]%c -> %M";
logger.AddFormat("INFO",    fmt, Colour.CYAN);
logger.AddFormat("SUCCESS", fmt, Colour.GREEN);
logger.AddFormat("WARN",    fmt, Colour.YELLOW);
logger.AddFormat("ERROR",   fmt, Colour.RED);

logger.Log("INFO",    "Initializing core subsystem components.");
logger.Log("SUCCESS", "Database connection established smoothly.");
logger.Log("WARN",    "High memory usage detected on node cluster.");
logger.Log("ERROR",   "Failed to write to write-ahead log!");
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

void AddFormat(string name, string formatString, string levelColour)

Register a named log level. Formats and FormatColours are static Dictionary instances shared across all Logger instances, so formats registered on one instance are visible to all others. Calling again with the same name replaces the entry and recalculates _maxNameLength.

Parameter Description
name Level identifier. Passed back via %N.
formatString Format string using the specifiers above.
levelColour One of the Colour. constants. Emitted by %C.

void AddFormat(string name, string formatString)

Overload that defaults levelColour to Colour.WHITE.

void Log(string formatName, string message)

Emit a log line. Source location is resolved via new StackTrace(true) -- frame index 1 is always the direct caller of Log(). Requires PDB symbols or source-linked builds for %F and %L to resolve correctly.

Note: In Release builds without PDBs, %F and %L may return null or 0. Either ship PDBs alongside the binary or restrict source-location specifiers to Debug configurations.

void ChangeDateFormat(string format)

Accepts "au" (DD/MM/YYYY) or "us" (MM/DD/YYYY). Any other value is silently ignored.

Colour.RESET    // \x1B[0m
Colour.BLACK    // \x1B[30m
Colour.RED      // \x1B[31m
Colour.GREEN    // \x1B[32m
Colour.YELLOW   // \x1B[33m
Colour.BLUE     // \x1B[34m
Colour.MAGENTA  // \x1B[35m
Colour.CYAN     // \x1B[36m
Colour.WHITE    // \x1B[37m
Colour.GREY     // \x1B[90m

All constants are public static readonly string on the Colour class in namespace include.

Formats and FormatColours are static dictionaries. AddFormat is not thread-safe and must be called during initialisation before spawning threads. Log() only reads from those dictionaries after setup, which is safe for concurrent readers.


Minimum: Java 21 | Files: Logger.java + Colour.java | Package: include

Place both files under src/include/. Import them into any class that needs logging.

import include.Logger;
import include.Colour;

public class main {
    public static void main(String[] args) {
        Logger logger = new Logger();

        String fmt = "%C[%N]%c%S %G[%D %T %Z]%c -> %M";
        logger.addFormat("INFO",    fmt, Colour.CYAN);
        logger.addFormat("SUCCESS", fmt, Colour.GREEN);
        logger.addFormat("WARN",    fmt, Colour.YELLOW);
        logger.addFormat("ERROR",   fmt, Colour.RED);

        logger.log("INFO",    "Initializing core subsystem components.");
        logger.log("SUCCESS", "Database connection established smoothly.");
        logger.log("WARN",    "High memory usage detected on node cluster.");
        logger.log("ERROR",   "Failed to write to write-ahead log!");
    }
}
# From the directory containing src/
javac -d out src/include/Colour.java src/include/Logger.java src/main.java
java -cp out main

void addFormat(String name, String formatString, CharSequence levelColour)

Register a named log level. FORMATS and FORMAT_COLOURS are static HashMap instances shared across all Logger instances. Calling again with the same name replaces the entry and recalculates maxNameLength.

Parameter Description
name Level identifier. Passed back via %N.
formatString Format string using the specifiers above.
levelColour A CharSequence -- in practice one of the Colour.* string constants. Emitted by %C.

void addFormat(String name, String formatString)

Overload that defaults levelColour to Colour.WHITE.

void log(String formatName, String message)

Emit a log line. Source location is resolved by calling Thread.currentThread().getStackTrace() and reading index 2 (0 = getStackTrace, 1 = log(), 2 = caller). The file name from getFileName() is the simple filename without path.

Note: Thread.currentThread().threadId() requires Java 19+, stable in Java 21. If you need Java 17 support, replace threadId() with the deprecated getId().

void changeDateFormat(String type)

Accepts "au" (DD/MM/YYYY) or "us" (MM/DD/YYYY). Uses "au".equals(type) null-safe comparison. Any other value is silently ignored.

Colour.RESET    // \033[0m
Colour.BLACK    // \033[30m
Colour.RED      // \033[31m
Colour.GREEN    // \033[32m
Colour.YELLOW   // \033[33m
Colour.BLUE     // \033[34m
Colour.MAGENTA  // \033[35m
Colour.CYAN     // \033[36m
Colour.WHITE    // \033[37m
Colour.GREY     // \033[90m

All constants are public static final String on the Colour class in package include.

FORMATS and FORMAT_COLOURS are static HashMap instances. addFormat is not thread-safe and must not be called concurrently. log() is effectively read-only on those maps after setup and is safe to call from multiple threads, though walking the stack trace on every call carries overhead under high concurrency.


This library is distributed under the GNU Lesser General Public License v2.1.

What LGPL v2.1 means in practice:

  • You may use Light-Weight-Logger in a closed-source application without being required to open-source that application
  • If you distribute a modified version of Light-Weight-Logger itself, those modifications must be released under LGPL v2.1
  • You must provide a way for end users to re-link your application against a modified version of the library
  • A copy of the licence text must accompany any distribution of the library

Copyright (C) 2026 David S

This library is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with this library. If not, see https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html#SEC1.

联系我们 contact @ memedata.com