diff options
Diffstat (limited to 'src/log.c')
| -rw-r--r-- | src/log.c | 376 |
1 files changed, 376 insertions, 0 deletions
diff --git a/src/log.c b/src/log.c new file mode 100644 index 0000000..0b663e7 --- /dev/null +++ b/src/log.c @@ -0,0 +1,376 @@ +#include <assert.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/uio.h> +#include <time.h> +#include <unistd.h> + +#ifdef CONFIG_OPENGL +#include <GL/gl.h> +#include "backend/gl/gl_common.h" +#include "backend/gl/glx.h" +#endif + +#include "compiler.h" +#include "log.h" +#include "utils.h" + +thread_local struct log *tls_logger; + +struct log_target; + +struct log { + struct log_target *head; + + int log_level; +}; + +struct log_target { + const struct log_ops *ops; + struct log_target *next; +}; + +struct log_ops { + void (*write)(struct log_target *, const char *, size_t); + void (*writev)(struct log_target *, const struct iovec *, int vcnt); + void (*destroy)(struct log_target *); + + /// Additional strings to print around the log_level string + const char *(*colorize_begin)(enum log_level); + const char *(*colorize_end)(enum log_level); +}; + +/// Fallback writev for targets don't implement it +static attr_unused void +log_default_writev(struct log_target *tgt, const struct iovec *vec, int vcnt) { + size_t total = 0; + for (int i = 0; i < vcnt; i++) { + total += vec[i].iov_len; + } + + if (!total) { + // Nothing to write + return; + } + char *buf = ccalloc(total, char); + total = 0; + for (int i = 0; i < vcnt; i++) { + memcpy(buf + total, vec[i].iov_base, vec[i].iov_len); + total += vec[i].iov_len; + } + tgt->ops->write(tgt, buf, total); + free(buf); +} + +static attr_const const char *log_level_to_string(enum log_level level) { + switch (level) { + case LOG_LEVEL_TRACE: return "TRACE"; + case LOG_LEVEL_DEBUG: return "DEBUG"; + case LOG_LEVEL_INFO: return "INFO"; + case LOG_LEVEL_WARN: return "WARN"; + case LOG_LEVEL_ERROR: return "ERROR"; + case LOG_LEVEL_FATAL: return "FATAL ERROR"; + default: return "????"; + } +} + +enum log_level string_to_log_level(const char *str) { + if (strcasecmp(str, "TRACE") == 0) + return LOG_LEVEL_TRACE; + else if (strcasecmp(str, "DEBUG") == 0) + return LOG_LEVEL_DEBUG; + else if (strcasecmp(str, "INFO") == 0) + return LOG_LEVEL_INFO; + else if (strcasecmp(str, "WARN") == 0) + return LOG_LEVEL_WARN; + else if (strcasecmp(str, "ERROR") == 0) + return LOG_LEVEL_ERROR; + return LOG_LEVEL_INVALID; +} + +struct log *log_new(void) { + auto ret = cmalloc(struct log); + ret->log_level = LOG_LEVEL_WARN; + ret->head = NULL; + return ret; +} + +void log_add_target(struct log *l, struct log_target *tgt) { + assert(tgt->ops->writev); + tgt->next = l->head; + l->head = tgt; +} + +/// Remove a previously added log target for a log struct, and destroy it. If the log +/// target was never added, nothing happens. +void log_remove_target(struct log *l, struct log_target *tgt) { + struct log_target *now = l->head, **prev = &l->head; + while (now) { + if (now == tgt) { + *prev = now->next; + tgt->ops->destroy(tgt); + break; + } + prev = &now->next; + now = now->next; + } +} + +/// Destroy a log struct and every log target added to it +void log_destroy(struct log *l) { + // free all tgt + struct log_target *head = l->head; + while (head) { + auto next = head->next; + head->ops->destroy(head); + head = next; + } + free(l); +} + +void log_set_level(struct log *l, int level) { + assert(level <= LOG_LEVEL_FATAL && level >= 0); + l->log_level = level; +} + +enum log_level log_get_level(const struct log *l) { + return l->log_level; +} + +attr_printf(4, 5) void log_printf(struct log *l, int level, const char *func, + const char *fmt, ...) { + assert(level <= LOG_LEVEL_FATAL && level >= 0); + if (level < l->log_level) + return; + + char *buf = NULL; + va_list args; + + va_start(args, fmt); + int blen = vasprintf(&buf, fmt, args); + va_end(args); + + if (blen < 0 || !buf) { + free(buf); + return; + } + + struct timespec ts; + timespec_get(&ts, TIME_UTC); + struct tm now; + localtime_r(&ts.tv_sec, &now); + char time_buf[100]; + strftime(time_buf, sizeof time_buf, "%x %T", &now); + + char *time = NULL; + int tlen = asprintf(&time, "%s.%03ld", time_buf, ts.tv_nsec / 1000000); + if (tlen < 0 || !time) { + free(buf); + free(time); + return; + } + + const char *log_level_str = log_level_to_string(level); + size_t llen = strlen(log_level_str); + size_t flen = strlen(func); + + struct log_target *head = l->head; + while (head) { + const char *p = "", *s = ""; + size_t plen = 0, slen = 0; + + if (head->ops->colorize_begin) { + // construct target specific prefix + p = head->ops->colorize_begin(level); + plen = strlen(p); + if (head->ops->colorize_end) { + s = head->ops->colorize_end(level); + slen = strlen(s); + } + } + head->ops->writev( + head, + (struct iovec[]){{.iov_base = "[ ", .iov_len = 2}, + {.iov_base = time, .iov_len = (size_t)tlen}, + {.iov_base = " ", .iov_len = 1}, + {.iov_base = (void *)func, .iov_len = flen}, + {.iov_base = " ", .iov_len = 1}, + {.iov_base = (void *)p, .iov_len = plen}, + {.iov_base = (void *)log_level_str, .iov_len = llen}, + {.iov_base = (void *)s, .iov_len = slen}, + {.iov_base = " ] ", .iov_len = 3}, + {.iov_base = buf, .iov_len = (size_t)blen}, + {.iov_base = "\n", .iov_len = 1}}, + 11); + head = head->next; + } + free(time); + free(buf); +} + +/// A trivial deinitializer that simply frees the memory +static attr_unused void logger_trivial_destroy(struct log_target *tgt) { + free(tgt); +} + +/// A null log target that does nothing +static const struct log_ops null_logger_ops; +static struct log_target null_logger_target = { + .ops = &null_logger_ops, +}; + +struct log_target *null_logger_new(void) { + return &null_logger_target; +} + +static void null_logger_write(struct log_target *tgt attr_unused, + const char *str attr_unused, size_t len attr_unused) { + return; +} + +static void null_logger_writev(struct log_target *tgt attr_unused, + const struct iovec *vec attr_unused, int vcnt attr_unused) { + return; +} + +static const struct log_ops null_logger_ops = { + .write = null_logger_write, + .writev = null_logger_writev, +}; + +/// A file based logger that writes to file (or stdout/stderr) +struct file_logger { + struct log_target tgt; + FILE *f; + struct log_ops ops; +}; + +static void file_logger_write(struct log_target *tgt, const char *str, size_t len) { + auto f = (struct file_logger *)tgt; + fwrite(str, 1, len, f->f); +} + +static void file_logger_writev(struct log_target *tgt, const struct iovec *vec, int vcnt) { + auto f = (struct file_logger *)tgt; + fflush(f->f); + writev(fileno(f->f), vec, vcnt); +} + +static void file_logger_destroy(struct log_target *tgt) { + auto f = (struct file_logger *)tgt; + fclose(f->f); + free(tgt); +} + +#define ANSI(x) "\033[" x "m" +static const char *terminal_colorize_begin(enum log_level level) { + switch (level) { + case LOG_LEVEL_TRACE: return ANSI("30;2"); + case LOG_LEVEL_DEBUG: return ANSI("37;2"); + case LOG_LEVEL_INFO: return ANSI("92"); + case LOG_LEVEL_WARN: return ANSI("33"); + case LOG_LEVEL_ERROR: return ANSI("31;1"); + case LOG_LEVEL_FATAL: return ANSI("30;103;1"); + default: return ""; + } +} + +static const char *terminal_colorize_end(enum log_level level attr_unused) { + return ANSI("0"); +} +#undef PREFIX + +static const struct log_ops file_logger_ops = { + .write = file_logger_write, + .writev = file_logger_writev, + .destroy = file_logger_destroy, +}; + +struct log_target *file_logger_new(const char *filename) { + FILE *f = fopen(filename, "a"); + if (!f) { + return NULL; + } + + auto ret = cmalloc(struct file_logger); + ret->tgt.ops = &ret->ops; + ret->f = f; + + // Always assume a file is not a terminal + ret->ops = file_logger_ops; + + return &ret->tgt; +} + +struct log_target *stderr_logger_new(void) { + int fd = dup(STDERR_FILENO); + if (fd < 0) { + return NULL; + } + + FILE *f = fdopen(fd, "w"); + if (!f) { + return NULL; + } + + auto ret = cmalloc(struct file_logger); + ret->tgt.ops = &ret->ops; + ret->f = f; + ret->ops = file_logger_ops; + + if (isatty(fd)) { + ret->ops.colorize_begin = terminal_colorize_begin; + ret->ops.colorize_end = terminal_colorize_end; + } + return &ret->tgt; +} + +#ifdef CONFIG_OPENGL +/// An opengl logger that can be used for logging into opengl debugging tools, +/// such as apitrace +struct gl_string_marker_logger { + struct log_target tgt; + PFNGLSTRINGMARKERGREMEDYPROC gl_string_marker; +}; + +static void +gl_string_marker_logger_write(struct log_target *tgt, const char *str, size_t len) { + auto g = (struct gl_string_marker_logger *)tgt; + // strip newlines at the end of the string + while (len > 0 && str[len-1] == '\n') { + len--; + } + g->gl_string_marker((GLsizei)len, str); +} + +static const struct log_ops gl_string_marker_logger_ops = { + .write = gl_string_marker_logger_write, + .writev = log_default_writev, + .destroy = logger_trivial_destroy, +}; + +struct log_target *gl_string_marker_logger_new(void) { + if (!gl_has_extension("GL_GREMEDY_string_marker")) { + return NULL; + } + + void *fnptr = glXGetProcAddress((GLubyte *)"glStringMarkerGREMEDY"); + if (!fnptr) + return NULL; + + auto ret = cmalloc(struct gl_string_marker_logger); + ret->tgt.ops = &gl_string_marker_logger_ops; + ret->gl_string_marker = fnptr; + return &ret->tgt; +} + +#else +struct log_target *gl_string_marker_logger_new(void) { + return NULL; +} +#endif + +// vim: set noet sw=8 ts=8: |