Logging in C with support for monoid logging
logging jsonl monoid c
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: Ready, set, GO!

author
Sona Tau Estrada Rivera
date (May 18, 2026, 5:12 PM -0400) commit 08759e5c
+887
+2
.clangd
··· 1 + CompileFlags: 2 + Add: [-Iinclude, -std=c23]
+1
.envrc
··· 1 + use flake
+17
.forgejo/workflows/ci.yml
··· 1 + name: ci 2 + 3 + on: [push, pull_request] 4 + 5 + jobs: 6 + test: 7 + runs-on: ubuntu-latest 8 + steps: 9 + - uses: actions/checkout@v4 10 + 11 + - name: install just 12 + run: | 13 + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh \ 14 + | bash -s -- --to ~/.local/bin 15 + echo "$HOME/.local/bin" >> "$GITHUB_PATH" 16 + 17 + - run: just test
+5
.gitignore
··· 1 + .direnv/ 2 + .build/ 3 + .cache/ 4 + compile_commands.json 5 + *.tar.gz
+8
README.md
··· 1 + # clog 2 + 3 + ```sh 4 + just # build static + dynamic 5 + just test # run tests 6 + just static # build .build/liblog.a 7 + just dynamic # build .build/liblog.so 8 + ```
+25
flake.lock
··· 1 + { 2 + "nodes": { 3 + "nixpkgs": { 4 + "locked": { 5 + "lastModified": 1778869304, 6 + "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", 7 + "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", 8 + "revCount": 998534, 9 + "type": "tarball", 10 + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.998534%2Brev-d233902339c02a9c334e7e593de68855ad26c4cb/019e3132-2bc3-7cf0-a522-2d02374629cf/source.tar.gz" 11 + }, 12 + "original": { 13 + "type": "tarball", 14 + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1" 15 + } 16 + }, 17 + "root": { 18 + "inputs": { 19 + "nixpkgs": "nixpkgs" 20 + } 21 + } 22 + }, 23 + "root": "root", 24 + "version": 7 25 + }
+46
flake.nix
··· 1 + { 2 + description = "Declarations for the environment that this project will use."; 3 + 4 + # Flake inputs 5 + inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1"; 6 + 7 + # Flake outputs 8 + outputs = inputs: 9 + let 10 + # The systems supported for this flake 11 + supportedSystems = [ 12 + "x86_64-linux" # 64-bit Intel/AMD Linux 13 + "aarch64-linux" # 64-bit ARM Linux 14 + "x86_64-darwin" # 64-bit Intel macOS 15 + "aarch64-darwin" # 64-bit ARM macOS 16 + ]; 17 + 18 + # Helper to provide system-specific attributes 19 + forEachSupportedSystem = f: inputs.nixpkgs.lib.genAttrs supportedSystems (system: f { 20 + pkgs = import inputs.nixpkgs { inherit system; }; 21 + }); 22 + in 23 + { 24 + devShells = forEachSupportedSystem ({ pkgs }: { 25 + default = pkgs.mkShell { 26 + # The Nix packages provided in the environment 27 + # Add any you need here 28 + packages = with pkgs; [ 29 + gcc 30 + just 31 + bear 32 + cppcheck 33 + doxygen 34 + clang-tools # provides clang-format and clangd 35 + ] ++ lib.optionals stdenv.isLinux [ valgrind ]; 36 + 37 + # Set any environment variables for your dev shell 38 + env = { }; 39 + 40 + # Add any shell logic you want executed any time the environment is activated 41 + shellHook = '' 42 + ''; 43 + }; 44 + }); 45 + }; 46 + }
+44
include/clog.h
··· 1 + #pragma once 2 + #include <stddef.h> 3 + 4 + #define CLOG_MSG_MAX 0x1000 5 + 6 + typedef enum { 7 + CLOG_FMT_JSONL = 0, 8 + CLOG_FMT_TEXT = 1, 9 + CLOG_FMT_TEXT_COLOR = 2, 10 + } ClogFmt; 11 + 12 + typedef enum { 13 + CLOG_INF = 0, 14 + CLOG_DBG = 1, 15 + CLOG_WRN = 2, 16 + CLOG_ERR = 3, 17 + } ClogLevel; 18 + 19 + typedef struct ClogEntry { 20 + ClogLevel level; 21 + char ts[26]; /* ctime() output, newline stripped */ 22 + const char *file; /* points to __FILE__ string literal */ 23 + unsigned int line; /* __LINE__ */ 24 + const char *func; /* points to __func__ string literal */ 25 + char msg[CLOG_MSG_MAX]; 26 + } ClogEntry; 27 + 28 + typedef ClogEntry *CLog; 29 + 30 + void clog_set_fmt(ClogFmt fmt); 31 + 32 + CLog clog_log_init(void); 33 + void clog_log_free(CLog log); 34 + void clog_log_print(const CLog log); 35 + 36 + /* internal — do not call directly */ 37 + void _clog_write_imm(ClogLevel level, const char *file, unsigned int line, const char *func, const char *msg); 38 + void _clog_write_log(CLog *log, ClogLevel level, const char *file, unsigned int line, const char *func, const char *msg); 39 + 40 + /* imperative */ 41 + #define clog(lvl, msg) _clog_write_imm((lvl), __FILE__, __LINE__, __func__, (msg)) 42 + 43 + /* monoid */ 44 + #define clogm(lvl, log, msg) _clog_write_log(&(log), (lvl), __FILE__, __LINE__, __func__, (msg))
+206
include/dynarr.h
··· 1 + #pragma once 2 + #include <assert.h> 3 + #include <stddef.h> 4 + #include <stdlib.h> 5 + #include <string.h> 6 + #include "prelude.h" 7 + 8 + /// @file dynarr.h 9 + /// @brief Type-safe dynamic array using a header-behind-pointer layout. 10 + /// 11 + /// Each array is a single heap allocation: a DynArrHeader immediately followed 12 + /// by the element data. The pointer you hold points to the data region, so 13 + /// plain C array indexing (@c arr[i]) works directly. 14 + /// 15 + /// @code 16 + /// int *nums = da_new(int); 17 + /// da_append(nums, 42); 18 + /// printf("%d\n", nums[0]); 19 + /// da_free(nums); 20 + /// @endcode 21 + /// 22 + /// @note @p arr must always be a typed pointer variable — many macros may 23 + /// reassign it when a reallocation moves the underlying block. 24 + 25 + /// @brief Internal metadata stored immediately before the data pointer. 26 + typedef struct { 27 + size_t count; ///< Number of elements currently stored. 28 + size_t capacity; ///< Number of elements the allocation can hold. 29 + } DynArrHeader; 30 + 31 + /// @cond INTERNAL 32 + void* new_header(void); 33 + void* copy_header(void *arr, size_t elem_size); 34 + 35 + /// @brief Retrieve the DynArrHeader that precedes @p arr in memory. 36 + #define da_header(arr) ((DynArrHeader*)(arr) - 1) 37 + /// @endcond 38 + 39 + // --------------------------------------------------------------------------- 40 + // Lifecycle 41 + // --------------------------------------------------------------------------- 42 + 43 + /// @brief Allocate a new empty dynamic array. 44 + /// 45 + /// @param type The element type (e.g. @c int, @c float, a struct). Used only 46 + /// as documentation; the initial allocation contains no elements. 47 + /// @return A @c void* pointing to the (empty) data region. Assign to a typed 48 + /// pointer: @code int *arr = da_new(int); @endcode 49 + /// @note Aborts on allocation failure. 50 + #define da_new(type) new_header() 51 + 52 + /// @brief Free a dynamic array. 53 + /// 54 + /// Releases the entire allocation (header + data). @p arr must not be used 55 + /// after this call. 56 + /// 57 + /// @param arr The array to free. 58 + #define da_free(arr) do { \ 59 + free(da_header(arr)); \ 60 + } while (0) 61 + 62 + /// @brief Return an independent copy of @p arr. 63 + /// 64 + /// Allocates a fresh block and copies all elements into it. The copy is 65 + /// @e tight: its capacity equals its count. Mutations to either array do not 66 + /// affect the other. 67 + /// 68 + /// @param arr The source array. 69 + /// @return A new @c void* pointing to the copied data. Assign to a typed 70 + /// pointer of the same type as @p arr. 71 + /// @note Aborts on allocation failure. 72 + #define da_copy(arr) copy_header((void*)(arr), sizeof(*(arr))) 73 + 74 + // --------------------------------------------------------------------------- 75 + // Adding elements 76 + // --------------------------------------------------------------------------- 77 + 78 + /// @brief Append a single element to the end of the array. 79 + /// 80 + /// Grows the allocation when @c count == @c capacity. Initial capacity is 8; 81 + /// subsequent growth adds 50% (@c capacity + @c capacity/2). 82 + /// 83 + /// @param arr The array. May be reassigned if a reallocation occurs. 84 + /// @param elem The value to append (evaluated once). 85 + /// @note Aborts if @p arr is @c NULL or if reallocation fails. 86 + #define da_append(arr, elem) do { \ 87 + exists(arr); \ 88 + DynArrHeader* h = da_header(arr); \ 89 + if (h->count == h->capacity) { \ 90 + size_t new_cap = h->capacity == 0 ? 8 : h->capacity + h->capacity / 2; \ 91 + void*tmp = realloc(h, sizeof(DynArrHeader) + new_cap * sizeof(elem)); \ 92 + exists(tmp); \ 93 + h = tmp; \ 94 + h->capacity = new_cap; \ 95 + } \ 96 + arr = (void*)(h + 1); \ 97 + arr[h->count++] = elem; \ 98 + } while (0) 99 + 100 + /// @brief Append @p n elements from a plain C array. 101 + /// 102 + /// Equivalent to calling da_append() in a loop but more efficient: at most 103 + /// one reallocation is performed. 104 + /// 105 + /// @param arr The destination array. May be reassigned if a reallocation occurs. 106 + /// @param ptr Pointer to the source elements. 107 + /// @param n Number of elements to copy from @p ptr. 108 + /// @note Aborts on allocation failure. 109 + #define da_extend(arr, ptr, n) do { \ 110 + size_t _n = (n); \ 111 + size_t _count = da_header(arr)->count; \ 112 + da_reserve(arr, _count + _n); \ 113 + memcpy((arr) + _count, (ptr), _n * sizeof(*(arr))); \ 114 + da_header(arr)->count = _count + _n; \ 115 + } while (0) 116 + 117 + /// @brief Pre-allocate capacity for at least @p n elements. 118 + /// 119 + /// Ensures that at least @p n elements can be stored without further 120 + /// reallocation. A no-op if current capacity already meets or exceeds @p n. 121 + /// 122 + /// @param arr The array. May be reassigned if a reallocation occurs. 123 + /// @param n Minimum desired capacity. 124 + /// @note Aborts on allocation failure. 125 + #define da_reserve(arr, n) do { \ 126 + DynArrHeader* h = da_header(arr); \ 127 + if ((n) > h->capacity) { \ 128 + void* tmp = realloc(h, sizeof(DynArrHeader) + (n) * sizeof(*(arr))); \ 129 + exists(tmp); \ 130 + h = tmp; \ 131 + h->capacity = (n); \ 132 + arr = (void*)(h + 1); \ 133 + } \ 134 + } while (0) 135 + 136 + // --------------------------------------------------------------------------- 137 + // Removing elements 138 + // --------------------------------------------------------------------------- 139 + 140 + /// @brief Remove and return the last element. 141 + /// 142 + /// Decrements @c count and returns the value that was at @c arr[count]. 143 + /// Capacity is unchanged. 144 + /// 145 + /// @param arr The array. 146 + /// @return The removed element (lvalue). 147 + #define da_pop(arr) (arr)[--da_header(arr)->count] 148 + 149 + /// @brief O(1) unordered removal at index @p i. 150 + /// 151 + /// Overwrites @c arr[i] with the last element then decrements @c count. 152 + /// Does @e not preserve element order. Use da_last() to read the element 153 + /// before removal if you need its value. 154 + /// 155 + /// @param arr The array. 156 + /// @param i Index to remove. Bounds-checked with @c assert. 157 + #define da_swap_remove(arr, i) do { \ 158 + DynArrHeader* h = da_header(arr); \ 159 + assert((i) < h->count); \ 160 + (arr)[(i)] = (arr)[h->count - 1]; \ 161 + h->count--; \ 162 + } while (0) 163 + 164 + /// @brief Reset the element count to zero, keeping the allocation intact. 165 + /// 166 + /// After a clear, da_empty() returns true and the buffer can be reused 167 + /// without another allocation. 168 + /// 169 + /// @param arr The array. 170 + #define da_clear(arr) do { da_header(arr)->count = 0; } while (0) 171 + 172 + // --------------------------------------------------------------------------- 173 + // Accessing elements 174 + // --------------------------------------------------------------------------- 175 + 176 + /// @brief Bounds-checked element access via @c assert. 177 + /// 178 + /// @param arr The array. 179 + /// @param i Index (must be < da_length(@p arr)). 180 + /// @return The element at index @p i (lvalue). 181 + #define da_get(arr, i) (assert((i) < da_header(arr)->count), (arr)[(i)]) 182 + 183 + /// @brief The last element (no bounds check). 184 + /// 185 + /// @param arr The array. Must be non-empty. 186 + /// @return The element at @c arr[count - 1] (lvalue). 187 + #define da_last(arr) (arr)[da_header(arr)->count - 1] 188 + 189 + // --------------------------------------------------------------------------- 190 + // Inspecting state 191 + // --------------------------------------------------------------------------- 192 + 193 + /// @brief Number of elements currently stored. 194 + /// @param arr The array. 195 + /// @return @c count as @c size_t. 196 + #define da_length(arr) da_header(arr)->count 197 + 198 + /// @brief Current allocated capacity (elements, not bytes). 199 + /// @param arr The array. 200 + /// @return @c capacity as @c size_t. 201 + #define da_capacity(arr) da_header(arr)->capacity 202 + 203 + /// @brief Test whether the array has no elements. 204 + /// @param arr The array. 205 + /// @return @c 1 if @c count == 0, @c 0 otherwise. 206 + #define da_empty(arr) (da_header(arr)->count == 0)
+43
include/prelude.h
··· 1 + #pragma once 2 + #include <errno.h> 3 + #include <stdint.h> 4 + #include <stdio.h> 5 + #include <string.h> 6 + #include <stdlib.h> 7 + 8 + inline static void free_ptr(void* p) { free(*(void**)p); } 9 + #define autofree __attribute__((cleanup(free_ptr))) 10 + 11 + #define exists(a) \ 12 + do { \ 13 + if (a == NULL) { \ 14 + fprintf(stderr, "%s:%s:%d Null pointer encountered: %s\n", __FILE__, \ 15 + __func__, __LINE__, strerror(errno)); \ 16 + exit(EXIT_FAILURE); \ 17 + } \ 18 + } while (0) 19 + 20 + /// Macro for marking parts that have not been implemented. 21 + #define TODO \ 22 + do { \ 23 + fprintf(stderr, "%s:%s:%d Not yet implemented\n", __FILE__, __func__, \ 24 + __LINE__); \ 25 + exit(EXIT_FAILURE); \ 26 + } while (0) 27 + 28 + /// Macro para crashear el programa si una condicion no se cumple 29 + #define expect(cond, ...) \ 30 + do { \ 31 + if (!(cond)) { \ 32 + fprintf(stderr, __VA_ARGS__); \ 33 + exit(EXIT_FAILURE); \ 34 + } \ 35 + } while (0) 36 + 37 + /* ----- Types ----- */ 38 + 39 + /// Simple Byte type. Guaranteed to be exactly 8 bits. 40 + typedef uint8_t Byte; 41 + 42 + /// String type. 43 + typedef const char *Str;
+167
justfile
··· 1 + # --- settings --- 2 + 3 + name := "clog" 4 + std := "c23" 5 + cc := "gcc" 6 + 7 + # --- directories --- 8 + 9 + src_dir := "src" 10 + inc_dir := "include" 11 + lib_dir := "lib" 12 + test_dir := "tests" 13 + build_dir := ".build" 14 + 15 + # --- platform --- 16 + 17 + so_ext := if os() == "macos" { "dylib" } else { "so" } 18 + so_flags := if os() == "macos" { "-dynamiclib" } else { "-shared" } 19 + 20 + # --- cflags --- 21 + 22 + warn := "-Wall -Wextra -Wfloat-equal -Wundef -Wshadow -Wpointer-arith -Wwrite-strings -Wswitch-default -Wconversion -Wunreachable-code -pedantic" 23 + base := "-std=" + std + " " + warn + " -I" + inc_dir 24 + san := "-fsanitize=address,undefined" 25 + 26 + # Static: put .a files in lib/static/ and append -L lib/static -lfoo 27 + # Dynamic: put .so files in lib/dynamic/ and append -L lib/dynamic -lbar 28 + lnk := "-L" + lib_dir + "/static -L" + lib_dir + "/dynamic -lm" 29 + 30 + # --- build --- 31 + 32 + # Build both static and dynamic libraries (default) 33 + build: static dynamic 34 + 35 + [group('build')] 36 + static: 37 + #!/usr/bin/env bash 38 + set -euo pipefail 39 + mkdir -p {{build_dir}} 40 + objs=() 41 + for f in $(find {{src_dir}} -name '*.c'); do 42 + obj="{{build_dir}}/$(basename "$f" .c).o" 43 + echo {{cc}} {{base}} -O2 -DNDEBUG -fPIC -c "$f" -o "$obj" 44 + {{cc}} {{base}} -O2 -DNDEBUG -fPIC -c "$f" -o "$obj" 45 + objs+=("$obj") 46 + done 47 + ar rcs {{build_dir}}/lib{{name}}.a "${objs[@]}" 48 + echo "Built: {{build_dir}}/lib{{name}}.a" 49 + 50 + [group('build')] 51 + dynamic: 52 + #!/usr/bin/env bash 53 + set -euo pipefail 54 + mkdir -p {{build_dir}} 55 + objs=() 56 + for f in $(find {{src_dir}} -name '*.c'); do 57 + obj="{{build_dir}}/$(basename "$f" .c).o" 58 + {{cc}} {{base}} -O2 -DNDEBUG -fPIC -c "$f" -o "$obj" 59 + objs+=("$obj") 60 + done 61 + {{cc}} {{so_flags}} -o {{build_dir}}/lib{{name}}.{{so_ext}} "${objs[@]}" {{lnk}} 62 + echo "Built: {{build_dir}}/lib{{name}}.{{so_ext}}" 63 + 64 + # --- checks --- 65 + 66 + [group('check')] 67 + test: 68 + #!/usr/bin/env bash 69 + set -euo pipefail 70 + mkdir -p {{build_dir}}/tests 71 + srcs=$(find {{src_dir}} -name '*.c' | tr '\n' ' ') 72 + failed=0 73 + for f in $(find {{test_dir}} -name 'test_*.c'); do 74 + tname=$(basename "$f" .c) 75 + printf ' %-30s' "$tname" 76 + {{cc}} {{base}} -I{{test_dir}} -g -O1 {{san}} "$f" $srcs {{lnk}} \ 77 + -o {{build_dir}}/tests/"$tname" 78 + if {{build_dir}}/tests/"$tname"; then 79 + printf 'ok\n' 80 + else 81 + printf 'FAILED\n' 82 + failed=$((failed + 1)) 83 + fi 84 + done 85 + [ $failed -eq 0 ] || { echo "$failed test(s) failed"; exit 1; } 86 + 87 + # Run tests under Valgrind (Linux only) 88 + [group('check')] 89 + valgrind: 90 + #!/usr/bin/env bash 91 + set -euo pipefail 92 + mkdir -p {{build_dir}}/tests 93 + srcs=$(find {{src_dir}} -name '*.c' | tr '\n' ' ') 94 + for f in $(find {{test_dir}} -name 'test_*.c'); do 95 + tname=$(basename "$f" .c) 96 + echo "==> valgrind: $tname" 97 + {{cc}} {{base}} -I{{test_dir}} -g -O1 "$f" $srcs {{lnk}} \ 98 + -o {{build_dir}}/tests/"$tname"-vg 99 + valgrind --leak-check=full --show-leak-kinds=all \ 100 + --track-origins=yes --error-exitcode=1 \ 101 + {{build_dir}}/tests/"$tname"-vg 102 + done 103 + 104 + [group('check')] 105 + cppcheck: 106 + cppcheck --enable=all --std={{std}} --inconclusive --error-exitcode=1 \ 107 + -I {{inc_dir}} {{src_dir}}/ 108 + 109 + # --- formatting --- 110 + 111 + [group('fmt')] 112 + fmt: 113 + find {{src_dir}} {{inc_dir}} -name '*.[ch]' | xargs clang-format -i 114 + 115 + [group('fmt')] 116 + fmt-check: 117 + find {{src_dir}} {{inc_dir}} -name '*.[ch]' | xargs clang-format --dry-run --Werror 118 + 119 + # --- miscellaneous --- 120 + 121 + # Generate compile_commands.json for clangd (requires bear) 122 + compile-commands: 123 + bear -- just build 124 + 125 + # Generate HTML documentation in .build/docs/html/ 126 + docs: 127 + #!/usr/bin/env bash 128 + set -euo pipefail 129 + mkdir -p {{build_dir}}/docs 130 + doxygen - <<-'DOXYEOF' 131 + PROJECT_NAME = {{name}} 132 + INPUT = {{src_dir}} {{inc_dir}} 133 + OUTPUT_DIRECTORY = {{build_dir}}/docs 134 + GENERATE_HTML = YES 135 + GENERATE_LATEX = NO 136 + RECURSIVE = YES 137 + EXTRACT_ALL = YES 138 + QUIET = YES 139 + DOXYEOF 140 + echo "docs: {{build_dir}}/docs/html/index.html" 141 + 142 + # Pack include/ and built libs into a tarball for use in a binary project. 143 + # Usage: just pack [output.tar.gz [dirname]] 144 + # just pack -> mylib.tar.gz, extracts into cwd 145 + # just pack mylib.tar.gz mylib -> extracts into mylib/ subdirectory 146 + pack output="" prefix="": 147 + #!/usr/bin/env bash 148 + set -euo pipefail 149 + just static 150 + just dynamic 151 + out="{{output}}" 152 + [[ -z "$out" ]] && out="{{name}}.tar.gz" 153 + stage=$(mktemp -d) 154 + [[ -d "$stage" ]] || { echo "error: mktemp failed" >&2; exit 1; } 155 + trap 'rm -rf "$stage"' EXIT 156 + pfx="{{prefix}}" 157 + root="$stage${pfx:+/$pfx}" 158 + mkdir -p "$root/include" "$root/lib/static" "$root/lib/dynamic" 159 + cp -r {{inc_dir}}/. "$root/include/" 160 + [[ -f "{{build_dir}}/lib{{name}}.a" ]] && cp "{{build_dir}}/lib{{name}}.a" "$root/lib/static/" 161 + [[ -f "{{build_dir}}/lib{{name}}.so" ]] && cp "{{build_dir}}/lib{{name}}.so" "$root/lib/dynamic/" 162 + [[ -f "{{build_dir}}/lib{{name}}.dylib" ]] && cp "{{build_dir}}/lib{{name}}.dylib" "$root/lib/dynamic/" 163 + tar -czf "$out" -C "$stage" "${pfx:-.}" 164 + echo "packed: $out" 165 + 166 + clean: 167 + rm -rf {{build_dir}}
lib/dynamic/.gitkeep

This is a binary file and will not be displayed.

lib/dynamic/libdynarr.so

This is a binary file and will not be displayed.

lib/static/.gitkeep

This is a binary file and will not be displayed.

lib/static/libdynarr.a

This is a binary file and will not be displayed.

+135
src/clog.c
··· 1 + #include "clog.h" 2 + #include "dynarr.h" 3 + #include <stdio.h> 4 + #include <stdlib.h> 5 + #include <string.h> 6 + #include <time.h> 7 + 8 + static ClogFmt g_fmt = CLOG_FMT_JSONL; 9 + 10 + /// Set the global output format. Defaults to CLOG_FMT_JSONL. 11 + void clog_set_fmt(ClogFmt fmt) { g_fmt = fmt; } 12 + 13 + /* --- helpers ------------------------------------------------------------ */ 14 + 15 + static const char *level_str(ClogLevel level) { 16 + switch (level) { 17 + case CLOG_INF: return "INF"; 18 + case CLOG_DBG: return "DBG"; 19 + case CLOG_WRN: return "WRN"; 20 + case CLOG_ERR: return "ERR"; 21 + default: return "???"; 22 + } 23 + } 24 + 25 + static const char *level_color(ClogLevel level) { 26 + switch (level) { 27 + case CLOG_INF: return "\033[36m"; /* cyan */ 28 + case CLOG_DBG: return "\033[37m"; /* white */ 29 + case CLOG_WRN: return "\033[33m"; /* yellow */ 30 + case CLOG_ERR: return "\033[1;31m"; /* bold red */ 31 + default: return ""; 32 + } 33 + } 34 + 35 + /// Fill ts[26] with the current local time in ANSI C format, newline stripped. 36 + static void get_ts(char ts[26]) { 37 + time_t t = time(NULL); 38 + const char *s = ctime(&t); 39 + strncpy(ts, s, 25); 40 + ts[25] = '\0'; 41 + if (ts[24] == '\n') ts[24] = '\0'; 42 + } 43 + 44 + /// Write a JSON-escaped string to stdout without surrounding quotes. 45 + static void write_json_str(const char *s) { 46 + for (; *s; s++) { 47 + switch (*s) { 48 + case '"': fputs("\\\"", stdout); break; 49 + case '\\': fputs("\\\\", stdout); break; 50 + case '\n': fputs("\\n", stdout); break; 51 + case '\r': fputs("\\r", stdout); break; 52 + case '\t': fputs("\\t", stdout); break; 53 + default: 54 + if ((unsigned char)*s < 0x20) { 55 + printf("\\u%04x", (unsigned char)*s); 56 + } else { 57 + putchar(*s); 58 + } 59 + } 60 + } 61 + } 62 + 63 + /// Print a single ClogEntry to stdout using the active format. 64 + static void print_entry(const ClogEntry *e) { 65 + const char *lvl = level_str(e->level); 66 + switch (g_fmt) { 67 + case CLOG_FMT_JSONL: 68 + printf("{\"ts\":\""); 69 + write_json_str(e->ts); 70 + printf("\",\"level\":\"%s\",\"file\":\"", lvl); 71 + write_json_str(e->file); 72 + printf("\",\"line\":%u,\"func\":\"", e->line); 73 + write_json_str(e->func); 74 + printf("\",\"msg\":\""); 75 + write_json_str(e->msg); 76 + printf("\"}\n"); 77 + break; 78 + case CLOG_FMT_TEXT: 79 + printf("%s:%u:%s %s %s %s\n", e->file, e->line, e->func, e->ts, lvl, e->msg); 80 + break; 81 + case CLOG_FMT_TEXT_COLOR: 82 + printf("%s:%u:%s %s %s%s\033[0m %s\n", e->file, e->line, e->func, e->ts, level_color(e->level), lvl, e->msg); 83 + break; 84 + default: 85 + fprintf(stderr, "Incorrect CLog format has been set: %d\n", g_fmt); 86 + exit(EXIT_FAILURE); 87 + } 88 + } 89 + 90 + /// Build a ClogEntry from the given fields, capturing the current timestamp. 91 + static ClogEntry make_entry(ClogLevel level, const char *file, unsigned int line, const char *func, const char *msg) { 92 + ClogEntry e; 93 + e.level = level; 94 + get_ts(e.ts); 95 + e.file = file; 96 + e.line = line; 97 + e.func = func; 98 + strncpy(e.msg, msg, CLOG_MSG_MAX - 1); 99 + e.msg[CLOG_MSG_MAX - 1] = '\0'; 100 + return e; 101 + } 102 + 103 + /* --- lifecycle ---------------------------------------------------------- */ 104 + 105 + /// Allocate and return a new empty CLog (dynamic array of ClogEntry). 106 + CLog clog_log_init(void) { 107 + return da_new(ClogEntry); 108 + } 109 + 110 + /// Free a CLog and all its entries. 111 + void clog_log_free(CLog log) { 112 + da_free(log); 113 + } 114 + 115 + /// Print all entries in log to stdout using the active format. 116 + void clog_log_print(const CLog log) { 117 + size_t n = da_length(log); 118 + for (size_t i = 0; i < n; i++) { 119 + print_entry(&log[i]); 120 + } 121 + } 122 + 123 + /* --- write -------------------------------------------------------------- */ 124 + 125 + /// Immediately print a log entry to stdout. Called via the clog() macro. 126 + void _clog_write_imm(ClogLevel level, const char *file, unsigned int line, const char *func, const char *msg) { 127 + ClogEntry e = make_entry(level, file, line, func, msg); 128 + print_entry(&e); 129 + } 130 + 131 + /// Append a log entry to a CLog. Called via the clogm() macro. 132 + void _clog_write_log(CLog *log, ClogLevel level, const char *file, unsigned int line, const char *func, const char *msg) { 133 + ClogEntry e = make_entry(level, file, line, func, msg); 134 + da_append(*log, e); 135 + }
+23
src/dynarr.c
··· 1 + #include "dynarr.h" 2 + 3 + /// Allocate a new DynArrHeader with count=0 and capacity=0. 4 + /// Returns a pointer to the data region immediately following the header. 5 + void *new_header(void) { 6 + DynArrHeader *h = malloc(sizeof(DynArrHeader)); 7 + exists(h); 8 + h->count = 0; 9 + h->capacity = 0; 10 + return h + 1; 11 + } 12 + 13 + /// Return a tight copy of arr (capacity == count). 14 + /// elem_size is the size of one element in bytes. 15 + void *copy_header(void *arr, size_t elem_size) { 16 + DynArrHeader *src = da_header(arr); 17 + size_t bytes = sizeof(DynArrHeader) + src->count * elem_size; 18 + DynArrHeader *dst = malloc(bytes); 19 + exists(dst); 20 + memcpy(dst, src, bytes); 21 + dst->capacity = dst->count; 22 + return dst + 1; 23 + }
+18
tests/test.h
··· 1 + #pragma once 2 + #include <stdio.h> 3 + #include <stdlib.h> 4 + #include <string.h> 5 + 6 + #define ASSERT(cond) \ 7 + do { \ 8 + if (!(cond)) { \ 9 + fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \ 10 + exit(1); \ 11 + } \ 12 + } while (0) 13 + 14 + #define ASSERT_EQ(a, b) ASSERT((a) == (b)) 15 + #define ASSERT_NE(a, b) ASSERT((a) != (b)) 16 + #define ASSERT_STR_EQ(a, b) ASSERT(strcmp((a), (b)) == 0) 17 + #define ASSERT_NULL(p) ASSERT((p) == NULL) 18 + #define ASSERT_NOT_NULL(p) ASSERT((p) != NULL)
+136
tests/test_clog.c
··· 1 + #define _POSIX_C_SOURCE 200809L 2 + #include <stdio.h> 3 + #include <string.h> 4 + #include <unistd.h> 5 + #include "test.h" 6 + #include "clog.h" 7 + #include "dynarr.h" 8 + 9 + /* --- stdout capture helpers -------------------------------------------- */ 10 + 11 + static FILE *capture_begin(int *saved_fd) { 12 + FILE *tmp = tmpfile(); 13 + *saved_fd = dup(STDOUT_FILENO); 14 + dup2(fileno(tmp), STDOUT_FILENO); 15 + return tmp; 16 + } 17 + 18 + static void capture_end(FILE *tmp, int saved_fd, char *buf, size_t size) { 19 + fflush(stdout); 20 + dup2(saved_fd, STDOUT_FILENO); 21 + close(saved_fd); 22 + rewind(tmp); 23 + if (fgets(buf, (int)size, tmp) == NULL) buf[0] = '\0'; 24 + fclose(tmp); 25 + } 26 + 27 + /* --- monoid tests ------------------------------------------------------- */ 28 + 29 + static void test_init_empty(void) { 30 + CLog log = clog_log_init(); 31 + ASSERT_NOT_NULL(log); 32 + ASSERT_EQ(da_length(log), (size_t)0); 33 + clog_log_free(log); 34 + } 35 + 36 + static void test_append_count(void) { 37 + CLog log = clog_log_init(); 38 + clogm(CLOG_INF, log, "one"); 39 + clogm(CLOG_DBG, log, "two"); 40 + clogm(CLOG_ERR, log, "three"); 41 + ASSERT_EQ(da_length(log), (size_t)3); 42 + clog_log_free(log); 43 + } 44 + 45 + static void test_entry_fields(void) { 46 + CLog log = clog_log_init(); 47 + clogm(CLOG_WRN, log, "hello"); 48 + ClogEntry *e = &log[0]; 49 + ASSERT_EQ(e->level, CLOG_WRN); 50 + ASSERT_STR_EQ(e->msg, "hello"); 51 + ASSERT_NOT_NULL(e->file); 52 + ASSERT_NOT_NULL(e->func); 53 + ASSERT(e->line > 0); 54 + ASSERT(e->ts[0] != '\0'); 55 + clog_log_free(log); 56 + } 57 + 58 + static void test_msg_truncation(void) { 59 + CLog log = clog_log_init(); 60 + char big[CLOG_MSG_MAX + 64]; 61 + memset(big, 'a', sizeof(big) - 1); 62 + big[sizeof(big) - 1] = '\0'; 63 + clogm(CLOG_INF, log, big); 64 + ASSERT_EQ(strlen(log[0].msg), (size_t)(CLOG_MSG_MAX - 1)); 65 + clog_log_free(log); 66 + } 67 + 68 + /* --- format tests ------------------------------------------------------- */ 69 + 70 + static void test_jsonl_fields(void) { 71 + clog_set_fmt(CLOG_FMT_JSONL); 72 + int saved; char buf[4096]; 73 + FILE *tmp = capture_begin(&saved); 74 + clog(CLOG_ERR, "boom"); 75 + capture_end(tmp, saved, buf, sizeof(buf)); 76 + 77 + ASSERT_NOT_NULL(strstr(buf, "\"level\":\"ERR\"")); 78 + ASSERT_NOT_NULL(strstr(buf, "\"msg\":\"boom\"")); 79 + ASSERT_NOT_NULL(strstr(buf, "\"ts\":\"")); 80 + ASSERT_NOT_NULL(strstr(buf, "\"file\":\"")); 81 + ASSERT_NOT_NULL(strstr(buf, "\"func\":\"")); 82 + clog_set_fmt(CLOG_FMT_JSONL); 83 + } 84 + 85 + static void test_jsonl_escaping(void) { 86 + clog_set_fmt(CLOG_FMT_JSONL); 87 + int saved; char buf[4096]; 88 + FILE *tmp = capture_begin(&saved); 89 + clog(CLOG_INF, "say \"hello\""); 90 + capture_end(tmp, saved, buf, sizeof(buf)); 91 + 92 + ASSERT_NOT_NULL(strstr(buf, "say \\\"hello\\\"")); 93 + clog_set_fmt(CLOG_FMT_JSONL); 94 + } 95 + 96 + static void test_text_fields(void) { 97 + clog_set_fmt(CLOG_FMT_TEXT); 98 + int saved; char buf[4096]; 99 + FILE *tmp = capture_begin(&saved); 100 + clog(CLOG_INF, "hello world"); 101 + capture_end(tmp, saved, buf, sizeof(buf)); 102 + 103 + ASSERT_NOT_NULL(strstr(buf, "INF")); 104 + ASSERT_NOT_NULL(strstr(buf, "hello world")); 105 + clog_set_fmt(CLOG_FMT_JSONL); 106 + } 107 + 108 + static void test_print_smoke(void) { 109 + clog_set_fmt(CLOG_FMT_JSONL); 110 + CLog log = clog_log_init(); 111 + clogm(CLOG_INF, log, "a"); 112 + clogm(CLOG_WRN, log, "b"); 113 + int saved; char buf[4096]; 114 + FILE *tmp = capture_begin(&saved); 115 + clog_log_print(log); 116 + capture_end(tmp, saved, buf, sizeof(buf)); 117 + 118 + ASSERT_NOT_NULL(strstr(buf, "\"level\":\"INF\"")); 119 + clog_log_free(log); 120 + clog_set_fmt(CLOG_FMT_JSONL); 121 + } 122 + 123 + /* ----------------------------------------------------------------------- */ 124 + 125 + int main(void) { 126 + test_init_empty(); 127 + test_append_count(); 128 + test_entry_fields(); 129 + test_msg_truncation(); 130 + test_jsonl_fields(); 131 + test_jsonl_escaping(); 132 + test_text_fields(); 133 + test_print_smoke(); 134 + printf("test_clog: all tests passed\n"); 135 + return 0; 136 + }
+11
tests/test_log.c
··· 1 + #include <stdio.h> 2 + #include "test.h" 3 + #include "clog.h" 4 + 5 + /* Placeholder — tests for the original stub functions were removed when 6 + clog.c was replaced with the logging implementation. See test_clog.c. */ 7 + 8 + int main(void) { 9 + printf("test_log: all tests passed\n"); 10 + return 0; 11 + }