Lately I've been interested in creating an HTTP server written in C, mostly for learning purposes, but also as a jumping-off point for other projects. There are a lot of great resources on the topic, including an old reference implementation called nweb.
That being said, I figured a good place to start would be with logging.
Printf debugging
is my go-to for understanding what has gone wrong
with my code, as it so often does. I also find that significant use of debug
statements can really help with tracing the flow/execution of a program. And
while printf itself is quite simple to use, I've also been using
Golang for other projects recently and enjoying its structured logging
library, log/slog. As such, I figured
that I'd give an attempt at implementing something like it in C:
#include <stdarg.h>
#include <stdio.h>
// PP_NARG (and the other supporting macros defined before it) returns the
// number of arguments passed to a variadic argument macro, up to 63. Originally
// from https://groups.google.com/g/comp.std.c/c/d-6Mj5Lko_s
#define PP_ARG_N(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, \
_15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, \
_27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, \
_39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, \
_51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, \
_63, N, ...) \
N
#define PP_RSEQ_N() \
63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, \
45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, \
28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, \
11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
#define PP_NARG_(...) PP_ARG_N(__VA_ARGS__)
#define PP_NARG(...) PP_NARG_(__VA_ARGS__, PP_RSEQ_N())
// Convenience function for _fslog which automatically counts the number of
// input arguments.
#define fslog(f, msg, ...) _fslog(f, msg, PP_NARG(__VA_ARGS__) / 3, __VA_ARGS__)
#define slog(msg, ...) fslog(stdout, msg, __VA_ARGS__)
// Convenience macros to help organize the input arguments to the slog
// functions. Clang's autoformatter also does better with `slog_str(key, value)`
// than with `SLOG_STR, key, value`, since it will keep arguments together on
// the same line instead of splitting them across multiple lines arbitrarily.
#define slog_str(key, value) SLOG_STR, key, value
#define slog_int(key, value) SLOG_INT, key, value
#define slog_double(key, value) SLOG_DOUBLE, key, value
enum slog_type {
SLOG_STR,
SLOG_INT,
SLOG_DOUBLE,
};
// Structured logging to a file, inspired by Golang's log/slog library.
//
// As an alternative to variadic arguments, a pointer to a struct like the
// following could also be used:
//
// struct slog_value {
// enum slog_type type;
// const char *key;
// union {
// const char *slog_str;
// int slog_int;
// double slog_double;
// };
// };
//
// And the convenience macros would look something like:
//
// #define slog_str(_key, value)
// (struct slog_value) { .type = SLOG_STR, .key = _key, .slog_str = value }
void _fslog(FILE *file, const char *message, size_t num_args, ...) {
va_list vl;
va_start(vl, num_args);
fprintf(file, "{\"message\": \"%s\"", message);
for (size_t i = 0; i < num_args; i++) {
enum slog_type type = va_arg(vl, enum slog_type);
const char *key = va_arg(vl, const char *);
fputs(", ", file);
switch (type) {
case SLOG_STR: { // Nested scope inside of a switch statement allows for
// redeclaring a variable of the same name with
// different types. The proper way to do this is
// probably to declare the variables with different
// names before the switch statement, but I find this
// easier to read.
const char *value = va_arg(vl, const char *);
fprintf(file, "\"%s\": \"%s\"", key, value);
break;
}
case SLOG_INT: {
int value = va_arg(vl, int);
fprintf(file, "\"%s\": %d", key, value);
break;
}
case SLOG_DOUBLE: {
double value = va_arg(vl, double);
fprintf(file, "\"%s\": %f", key, value);
break;
}
default:
break;
}
}
fputs("}\n", file);
va_end(vl);
}
int main(void) {
// fprintf example:
fprintf(stdout,
"{\"message\": \"HTTP response\", \"method\": \"%s\", \"path\": "
"\"%s\", \"status\": %d}\n",
"GET", "/index.html", 200);
// fslog example:
fslog(stdout, "HTTP response", slog_str("method", "GET"),
slog_str("path", "/index.html"), slog_int("status", 200));
return 0;
}
All-in-all, I think that the final fslog example looks significantly nicer than the fprintf example, at least for JSON log format. A plain text log format probably wouldn't benefit as much, and admittedly I'll likely be using plain text log formats most of the time, so I'm not fully convinced that including all of this code (and more, if some of the extensions mentioned below are added) is worth while, especially if you want the final program code to be as minimalistic as possible.
Still, it's handy to know that if I do want to structure my printf
debugging
a little bit more in the future, that it's not too difficult
to do so. And I also learned a bit about variadic macros along the way.
If I were to extend this in the future, some other features that Golang's log/slog library has that would be worth adding include:
DEBUG
, INFO
,
WARN
, ERROR
.