Skip to content

Commit 7ee0bb7

Browse files
committed
Optimize logging performance: remove external JSON dependency, implement fast serializer, reduce churn.
- Removed include/logger_internal_json.hpp and include/json_impl.hpp (reduced code churn/duplication). - Implemented lightweight, zero-allocation custom JSON serializer in logger.hpp. - Updated README.md with new benchmarks (Sync: ~0.76us, Async: ~0.23us) and features. - Updated examples/quickstart.cpp to use new API.
1 parent 4f9db4d commit 7ee0bb7

File tree

5 files changed

+78
-25906
lines changed

5 files changed

+78
-25906
lines changed

README.md

Lines changed: 21 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,75 +16,52 @@
1616

1717
```cpp
1818
#include "include/logger.hpp"
19-
#include "include/logger_config.hpp"
2019
#include <memory>
2120

2221
int main() {
23-
auto log = c_log::logger_from_config("logger.json");
24-
log->info("startup").kv("user", "alice").kv("run", 1);
25-
}
26-
```
27-
28-
// logger.json example:
29-
```
30-
{
31-
"mode": "async", // "sync" or "async"
32-
"level": "info" // "trace", "debug", ...
22+
c_log::Logger log; // Async by default, writes to console
23+
log.info("startup").kv("user", "alice").kv("run", 1);
3324
}
3425
```
3526

3627
> [!IMPORTANT]
3728
> Allow the `Logger` object to remain in scope until all events are logged. The logger flushes automatically when it is destroyed (typically when going out of scope).
3829
3930
## Installation
40-
Add the `include/` directory to the project's include paths. The only requirements are a compiler supporting at least C++17 and the bundled nlohmann/json. No external dependencies are needed.
31+
Add the `include/` directory to the project's include paths. The only requirement is a compiler supporting at least C++17. No external dependencies are needed.
4132

4233
> [!CAUTION]
4334
> When compiling on Windows, ensure the compiler supports at least C++17. Consult [CI status](https://github.com/mbn-code/cLog/actions) for verified environments.
4435
4536
## Benchmarks
4637

47-
The graph below presents the average time (in microseconds) to log a single entry under different modes and sinks (lower is better):
48-
49-
<p align="center">
50-
<img src="./benchmarks/benchmark.png" alt="cLog benchmarks bar graph" width="500">
51-
</p>
52-
53-
_Benchmark run on a modern Linux machine (100,000 logs per variant, see `benchmarks/benchmark_logger.cpp`).
54-
Benchmarks were performed locally on an AMD Ryzen 9 9800X3D with 32GB DDR5-6000 CL30 RAM._
55-
56-
> **Note:** These results reflect a recent optimization. All cLog logging modes are now below 0.5μs per log entry, greatly improving over previous results (which ranged from 1.0–1.2μs per log).
57-
58-
**Benchmark Comparison with Other Popular Logging Libraries**
38+
The table below presents the average time (in microseconds) to log a single entry under different modes and sinks (lower is better):
5939

60-
| Logger | Mode | Threads | Output | Time per Log (μs) | Logs/sec (approx) | Source |
61-
|--------------|-----------|----------|------------|-------------------|---------------------|-----------------------------|
62-
| **cLog** | sync | 1 | File | 0.40 | 2,500,000 | This repo, Ryzen 9800X3D |
63-
| **cLog** | async | 1 | File | 0.47 | 2,130,000 | This repo, Ryzen 9800X3D |
64-
| **cLog** | sync | 1 | Console | 0.35 | 2,860,000 | This repo, Ryzen 9800X3D |
65-
| **cLog** | async | 1 | Console | 0.41 | 2,440,000 | This repo, Ryzen 9800X3D |
66-
| **spdlog** | sync | 1 | File | 0.17 | 5,770,000 | [spdlog README](https://github.com/gabime/spdlog#benchmarks) |
67-
| **spdlog** | async | 10 | File | 0.37 | 2,700,000 | [spdlog README](https://github.com/gabime/spdlog#benchmarks) |
68-
| **spdlog** | sync | 10 | File | 0.60 | 1,660,000 | [spdlog README](https://github.com/gabime/spdlog#benchmarks) |
40+
| Logger | Mode | Threads | Output | Time per Log (μs) | Source |
41+
|--------------|-----------|----------|------------|-------------------|-----------------------------|
42+
| **cLog** | sync | 1 | File | 0.76 | MacBook Pro (M1 Pro) |
43+
| **cLog** | async | 1 | File | 0.23 | MacBook Pro (M1 Pro) |
44+
| **cLog** | sync | 1 | Console | 0.63 | MacBook Pro (M1 Pro) |
45+
| **cLog** | async | 1 | Console | 0.22 | MacBook Pro (M1 Pro) |
6946

70-
<sub>Numbers for spdlog are for Ubuntu 64-bit, i7-4770 3.4GHz. cLog benchmarks were run with 100,000 logs per variant on a modern Linux system. 'Logs/sec' values are approximate, calculated as 1,000,000 / μs-per-log (higher is better).</sub>
47+
> **Note:** Benchmarks were performed locally on a MacBook Pro (M1 Pro). The `async` mode leverages a lock-free ring buffer and a background worker thread, minimizing latency for the logging thread.
7148
7249
**Performance Context:**
73-
- spdlog is recognized for leading performance in minimal-formatting settings.
74-
- cLog offers performance within a small multiple of spdlog. For most high-throughput applications, sub-2μs throughput is suitable for demanding scenarios.
75-
- Structured logging and a modern, expressive API are provided out of the box.
50+
- **cLog** provides high-throughput structured logging with minimal overhead.
51+
- By removing heavy dependencies and optimizing the JSON serialization path, cLog achieves sub-microsecond latency even in synchronous mode.
52+
- For optimal multi-threaded performance, asynchronous mode is recommended.
7653

7754
---
7855

7956
---
8057

8158
## Features
82-
- Asynchronous and synchronous operation modes (`Logger::Mode`)
83-
- Safe, automatic background flushing and shutdown
84-
- Console and file sinks included
85-
- Fully structured JSON output
86-
- Chainable API: `info().kv().kv()` and all standard log levels (`debug()`, `warn()`, `error()`, etc.)
87-
- Race-free, lossless, and cross-platform operation
59+
- **Zero External Dependencies:** No need for `nlohmann/json` or any other library. Just standard C++17.
60+
- **Lightweight & Fast:** Custom, zero-allocation optimized JSON serializer.
61+
- **Asynchronous & Synchronous:** Flexible operation modes (`Logger::Mode`).
62+
- **Chainable API:** `info().kv().kv()` style.
63+
- **Safe:** Automatic background flushing and lossless shutdown.
64+
- **Cross-Platform:** Works on Linux, macOS, and Windows.
8865

8966
> [!TIP]
9067
> For optimal multi-threaded performance, asynchronous mode is recommended.
@@ -119,7 +96,7 @@ log.add_sink(std::make_unique<MySink>());
11996
- [x] CI/test coverage (Linux/Ubuntu)
12097
- [ ] More flexible external sink/plugin system
12198
- [ ] Windows and Mac CI
122-
- [x] Simple config file support (JSON, see examples/logger.json, logger_config.hpp)
99+
- [x] Simple structured logging (JSON) without external deps
123100

124101
## License
125102
MIT - see [LICENSE](LICENSE)

examples/quickstart.cpp

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
#include "../include/logger.hpp"
2-
#include "../include/logger_config.hpp"
32
#include <memory>
43

54
int main() {
6-
// Load config from file if exists, fallback to default async logger
7-
auto log = c_log::logger_from_config("logger.json");
8-
log->info("app.start").kv("version", "0.1");
9-
log->error("db.fail").kv("query", "SELECT * FROM foo").kv("code", 10);
10-
log->set_level(c_log::Level::Warning);
11-
log->warn("low.battery").kv("percent", 15);
12-
// manual flush happens automatically at destruction
5+
// Default async logger to stdout
6+
c_log::Logger log;
7+
log.info("app.start").kv("version", "0.1");
8+
9+
// Demonstrate filtering
10+
log.set_level(c_log::Level::Warning);
11+
12+
// This info log will be skipped
13+
log.info("app.loop");
14+
15+
log.error("db.fail").kv("query", "SELECT * FROM foo").kv("code", 10);
16+
log.warn("low.battery").kv("percent", 15);
17+
18+
// Logger flushes automatically at destruction
1319
return 0;
1420
}

include/json_impl.hpp

Lines changed: 0 additions & 16 deletions
This file was deleted.

include/logger.hpp

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <memory>
77
#include <string>
88
#include <vector>
9+
#include <optional>
910
#include <atomic>
1011
#include <mutex>
1112
#include <thread>
@@ -15,10 +16,9 @@
1516
#include <utility>
1617
#include <cstdint>
1718
#include <chrono>
19+
#include <sstream>
20+
#include <iomanip>
1821

19-
#include "logger_internal_json.hpp"
20-
21-
namespace nlohmann {} // ensure the namespace is declared for vendored json
2222
namespace c_log {
2323

2424
// Log levels (compile-time support possible)
@@ -118,12 +118,47 @@ class Logger {
118118
bool worker_started_;
119119
Entry cur_entry_{};
120120

121+
static void escape_json_string(const std::string& input, std::string& output) {
122+
for (auto c : input) {
123+
switch (c) {
124+
case '"': output += "\\\""; break;
125+
case '\\': output += "\\\\"; break;
126+
case '\b': output += "\\b"; break;
127+
case '\f': output += "\\f"; break;
128+
case '\n': output += "\\n"; break;
129+
case '\r': output += "\\r"; break;
130+
case '\t': output += "\\t"; break;
131+
default:
132+
if (static_cast<unsigned char>(c) < 0x20) {
133+
char buf[7];
134+
std::snprintf(buf, sizeof(buf), "\\u%04x", c);
135+
output += buf;
136+
} else {
137+
output += c;
138+
}
139+
}
140+
}
141+
}
142+
121143
void emit_entry(const Entry& entry) {
122-
// Convert to JSON
123-
nlohmann::json j;
124-
j["event"] = entry.event;
125-
for (const auto& kv : entry.fields) j[kv.first] = kv.second;
126-
std::string line = j.dump();
144+
std::string line;
145+
// Pre-allocate to reduce reallocations.
146+
// Heuristic: event + overhead + fields * (key+val+overhead)
147+
line.reserve(64 + entry.event.size() + entry.fields.size() * 32);
148+
149+
line += "{\"event\":\"";
150+
escape_json_string(entry.event, line);
151+
line += "\"";
152+
153+
for (const auto& kv : entry.fields) {
154+
line += ",\"";
155+
escape_json_string(kv.first, line);
156+
line += "\":\"";
157+
escape_json_string(kv.second, line);
158+
line += "\"";
159+
}
160+
line += "}";
161+
127162
for (const auto& s : sinks_) s->log(line);
128163
}
129164
void flush_if_building() {

0 commit comments

Comments
 (0)