aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2021-01-13 20:00:25 +0100
committerDavid Robillard <d@drobilla.net>2021-03-08 23:23:06 -0500
commit468f7dc4294905d19f55c58c47ba91cd23bf357b (patch)
tree3531d4dbc53e3454534ea607dfa4f44d0f085f5d
parent229443778ffc6b26a13322983b81f4f1912427af (diff)
downloadserd-468f7dc4294905d19f55c58c47ba91cd23bf357b.tar.gz
serd-468f7dc4294905d19f55c58c47ba91cd23bf357b.tar.bz2
serd-468f7dc4294905d19f55c58c47ba91cd23bf357b.zip
WIP: Add extensible logging API
-rw-r--r--NEWS1
-rw-r--r--include/serd/serd.h132
-rw-r--r--src/node_syntax.c12
-rw-r--r--src/reader.c4
-rw-r--r--src/reader.h8
-rw-r--r--src/serd_config.h13
-rw-r--r--src/serdi.c10
-rw-r--r--src/world.c210
-rw-r--r--src/world.h32
-rw-r--r--src/writer.c40
10 files changed, 367 insertions, 95 deletions
diff --git a/NEWS b/NEWS
index d5c9d9e9..ca56e137 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,7 @@ serd (1.0.1) unstable;
* Add SerdBuffer for mutable buffers to keep SerdChunk const-correct
* Add SerdWorld for shared library state
+ * Add extensible logging API
* Add option for writing terse output without newlines
* Add support for parsing variables
* Add support for writing terse collections
diff --git a/include/serd/serd.h b/include/serd/serd.h
index 4cf00f02..04d8708b 100644
--- a/include/serd/serd.h
+++ b/include/serd/serd.h
@@ -67,6 +67,12 @@
SERD_API \
SERD_MALLOC_FUNC
+#if defined(__GNUC__)
+# define SERD_LOG_FUNC(fmt, arg1) __attribute__((format(printf, fmt, arg1)))
+#else
+# define SERD_LOG_FUNC(fmt, arg1)
+#endif
+
#ifdef __cplusplus
extern "C" {
# if defined(__GNUC__)
@@ -268,14 +274,6 @@ typedef struct {
size_t len; ///< Size of buffer in bytes
} SerdBuffer;
-/// An error description
-typedef struct {
- SerdStatus status; ///< Error code
- const SerdCursor* SERD_NULLABLE cursor; ///< Origin of error
- const char* SERD_NONNULL fmt; ///< Printf-style format string
- va_list* SERD_NONNULL args; ///< Arguments for fmt
-} SerdError;
-
/**
A parsed URI.
@@ -1050,15 +1048,6 @@ serd_node_compare(const SerdNode* SERD_NULLABLE a,
@{
*/
-/**
- Callback function for errors.
-
- @param handle Handle for user data.
- @param error Error description.
-*/
-typedef SerdStatus (*SerdErrorFunc)(void* SERD_NULLABLE handle,
- const SerdError* SERD_NONNULL error);
-
/// Type of a SerdEvent
typedef enum {
SERD_BASE = 1, ///< Base URI changed
@@ -1175,16 +1164,113 @@ const SerdNode* SERD_NONNULL
serd_world_get_blank(SerdWorld* SERD_NONNULL world);
/**
- Set a function to be called when errors occur.
+ @}
+ @defgroup serd_logging Logging
+ @{
+*/
+
+/// Log message level, compatible with syslog
+typedef enum {
+ SERD_LOG_LEVEL_EMERG, ///< Emergency, system is unusable
+ SERD_LOG_LEVEL_ALERT, ///< Action must be taken immediately
+ SERD_LOG_LEVEL_CRIT, ///< Critical condition
+ SERD_LOG_LEVEL_ERR, ///< Error
+ SERD_LOG_LEVEL_WARNING, ///< Warning
+ SERD_LOG_LEVEL_NOTICE, ///< Normal but significant condition
+ SERD_LOG_LEVEL_INFO, ///< Informational message
+ SERD_LOG_LEVEL_DEBUG ///< Debug message
+} SerdLogLevel;
+
+/**
+ A structured log field.
+
+ This can be used to pass additional information along with log messages.
+ Syslog-compatible keys should be used where possible, otherwise, keys should
+ be namespaced to prevent clashes.
+
+ Serd itself uses the following keys:
+ - ERRNO
+ - SERD_COL
+ - SERD_FILE
+ - SERD_LINE
+ - SERD_STATUS
+*/
+typedef struct {
+ const char* SERD_NONNULL key; ///< Field name
+ const char* SERD_NONNULL value; ///< Field value
+} SerdLogField;
+
+/**
+ A log entry (message).
+
+ This is the description of a log entry which is passed to log functions.
+ It is only valid in the stack frame it appears in, and may not be copied.
+*/
+typedef struct {
+ const char* SERD_NONNULL domain; ///< Library/program/module name
+ const SerdLogField* SERD_NULLABLE fields; ///< Extra log fields
+ const char* SERD_NONNULL fmt; ///< Printf-style format string
+ va_list* SERD_NONNULL args; ///< Arguments for `fmt`
+ SerdLogLevel level; ///< Log level
+ size_t n_fields; ///< Number of `fields`
+} SerdLogEntry;
+
+/**
+ Sink function for log messages.
+
+ @param handle Handle for user data.
+ @param entry Pointer to log entry description.
+*/
+typedef SerdStatus (*SerdLogFunc)(void* SERD_NULLABLE handle,
+ const SerdLogEntry* SERD_NONNULL entry);
+
+/// A SerdLogFunc that does nothing, for suppressing log output
+SERD_API
+SerdStatus
+serd_quiet_error_func(void* SERD_NULLABLE handle,
+ const SerdLogEntry* SERD_NONNULL entry);
+
+/// Return the value of the log field named `key`, or NULL if none exists
+SERD_PURE_API
+const char* SERD_NULLABLE
+serd_log_entry_get_field(const SerdLogEntry* SERD_NONNULL entry,
+ const char* SERD_NONNULL key);
+
+/**
+ Set a function to be called with log messages (typically errors).
- The `error_func` will be called with `handle` as its first argument. If
- no error function is set, errors are printed to stderr.
+ The `log_func` will be called with `handle` as its first argument. If
+ no function is set, messages are printed to stderr.
*/
SERD_API
void
-serd_world_set_error_func(SerdWorld* SERD_NONNULL world,
- SerdErrorFunc SERD_NULLABLE error_func,
- void* SERD_NULLABLE handle);
+serd_world_set_log_func(SerdWorld* SERD_NONNULL world,
+ SerdLogFunc SERD_NULLABLE log_func,
+ void* SERD_NULLABLE handle);
+
+/// Write a message to the log
+SERD_API
+SERD_LOG_FUNC(6, 0)
+SerdStatus
+serd_world_vlogf(const SerdWorld* SERD_NONNULL world,
+ const char* SERD_NONNULL domain,
+ SerdLogLevel level,
+ size_t n_fields,
+ const SerdLogField* SERD_NULLABLE fields,
+ const char* SERD_NONNULL fmt,
+ va_list args);
+
+/// Write a message to the log
+SERD_API
+SERD_LOG_FUNC(6, 7)
+SerdStatus
+serd_world_logf(const SerdWorld* SERD_NONNULL world,
+ const char* SERD_NONNULL domain,
+ SerdLogLevel level,
+ size_t n_fields,
+ const SerdLogField* SERD_NULLABLE fields,
+ const char* SERD_NONNULL fmt,
+ ...);
/**
@}
diff --git a/src/node_syntax.c b/src/node_syntax.c
index fc442f33..f14300e6 100644
--- a/src/node_syntax.c
+++ b/src/node_syntax.c
@@ -23,14 +23,6 @@
#include <string.h>
static SerdStatus
-quiet_error_func(void* const handle, const SerdError* const e)
-{
- (void)handle;
- (void)e;
- return SERD_SUCCESS;
-}
-
-static SerdStatus
on_node_string_event(void* const handle, const SerdEvent* const event)
{
if (event->type == SERD_STATEMENT) {
@@ -67,7 +59,7 @@ serd_node_from_syntax(const char* const str, const SerdSyntax syntax)
sink,
1024 + doc_len);
- serd_world_set_error_func(world, quiet_error_func, NULL);
+ serd_world_set_log_func(world, serd_quiet_error_func, NULL);
serd_reader_start(reader, source);
serd_reader_read_document(reader);
serd_reader_finish(reader);
@@ -90,7 +82,7 @@ serd_node_to_syntax(const SerdNode* const node, const SerdSyntax syntax)
SerdByteSink* const out = serd_byte_sink_new_buffer(&buffer);
SerdWriter* const writer = serd_writer_new(world, syntax, 0, env, out);
- serd_world_set_error_func(world, quiet_error_func, NULL);
+ serd_world_set_log_func(world, serd_quiet_error_func, NULL);
SerdStatus st = SERD_SUCCESS;
char* result = NULL;
diff --git a/src/reader.c b/src/reader.c
index ba3559bc..b7e18d2a 100644
--- a/src/reader.c
+++ b/src/reader.c
@@ -35,8 +35,8 @@ r_err(SerdReader* reader, SerdStatus st, const char* fmt, ...)
{
va_list args;
va_start(args, fmt);
- const SerdError e = {st, &reader->source->cur, fmt, &args};
- serd_world_error(reader->world, &e);
+ serd_world_vlogf_internal(
+ reader->world, st, SERD_LOG_LEVEL_ERR, &reader->source->cur, fmt, args);
va_end(args);
return st;
}
diff --git a/src/reader.h b/src/reader.h
index 8fba7504..26a19633 100644
--- a/src/reader.h
+++ b/src/reader.h
@@ -28,12 +28,6 @@
#include <stdint.h>
#include <stdio.h>
-#if defined(__GNUC__)
-# define SERD_LOG_FUNC(fmt, arg1) __attribute__((format(printf, fmt, arg1)))
-#else
-# define SERD_LOG_FUNC(fmt, arg1)
-#endif
-
#ifdef SERD_STACK_CHECK
# define SERD_STACK_ASSERT_TOP(reader, ref) \
assert(ref == reader->allocs[reader->n_allocs - 1]);
@@ -52,7 +46,7 @@ typedef struct {
struct SerdReaderImpl {
SerdWorld* world;
const SerdSink* sink;
- SerdErrorFunc error_func;
+ SerdLogFunc log_func;
void* error_handle;
SerdNode* rdf_first;
SerdNode* rdf_rest;
diff --git a/src/serd_config.h b/src/serd_config.h
index 7f58696e..12930b1b 100644
--- a/src/serd_config.h
+++ b/src/serd_config.h
@@ -51,6 +51,13 @@
# endif
# endif
+// POSIX.1-2001: isatty()
+# ifndef HAVE_ISATTY
+# if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L
+# define HAVE_ISATTY
+# endif
+# endif
+
// POSIX.1-2001: posix_fadvise()
# ifndef HAVE_POSIX_FADVISE
# ifndef __APPLE__
@@ -83,6 +90,12 @@
# define USE_FILENO 0
#endif
+#ifdef HAVE_ISATTY
+# define USE_ISATTY 1
+#else
+# define USE_ISATTY 0
+#endif
+
#ifdef HAVE_POSIX_FADVISE
# define USE_POSIX_FADVISE 1
#else
diff --git a/src/serdi.c b/src/serdi.c
index 78de1ad3..2eea3919 100644
--- a/src/serdi.c
+++ b/src/serdi.c
@@ -84,14 +84,6 @@ missing_arg(const char* name, char opt)
}
static SerdStatus
-quiet_error_func(void* const handle, const SerdError* const e)
-{
- (void)handle;
- (void)e;
- return SERD_SUCCESS;
-}
-
-static SerdStatus
read_file(SerdWorld* const world,
SerdSyntax syntax,
const SerdReaderFlags flags,
@@ -310,7 +302,7 @@ main(int argc, char** argv)
serd_writer_new(world, output_syntax, writer_flags, env, byte_sink);
if (quiet) {
- serd_world_set_error_func(world, quiet_error_func, NULL);
+ serd_world_set_log_func(world, serd_quiet_error_func, NULL);
}
SerdNode* root = serd_new_uri(SERD_MEASURE_STRING(root_uri));
diff --git a/src/world.c b/src/world.c
index d166b1d3..a6e32dc6 100644
--- a/src/world.c
+++ b/src/world.c
@@ -21,6 +21,11 @@
#include "cursor.h"
#include "namespaces.h"
#include "node.h"
+#include "serd_config.h"
+
+#if USE_FILENO && USE_ISATTY
+# include <unistd.h>
+#endif
#include <errno.h>
#include <stdarg.h>
@@ -30,32 +35,199 @@
#define BLANK_CHARS 12
+static inline int
+level_color(const SerdLogLevel level)
+{
+ switch (level) {
+ case SERD_LOG_LEVEL_EMERG:
+ case SERD_LOG_LEVEL_ALERT:
+ case SERD_LOG_LEVEL_CRIT:
+ case SERD_LOG_LEVEL_ERR:
+ return 31; // Red
+ case SERD_LOG_LEVEL_WARNING:
+ return 33; // Yellow
+ case SERD_LOG_LEVEL_NOTICE:
+ case SERD_LOG_LEVEL_INFO:
+ case SERD_LOG_LEVEL_DEBUG:
+ break;
+ }
+
+ return 1; // White
+}
+
+static inline bool
+terminal_supports_color(void)
+{
+ const char* const term = getenv("TERM");
+ return !getenv("NO_COLOR") && term && strstr(term, "color");
+}
+
+static inline bool
+serd_ansi_start(FILE* stream, int color, bool bold)
+{
+#if USE_FILENO && USE_ISATTY
+ if (isatty(fileno(stream)) && terminal_supports_color()) {
+ return fprintf(stream, bold ? "\033[0;%d;1m" : "\033[0;%dm", color);
+ }
+#endif
+
+ return 0;
+}
+
+static inline void
+serd_ansi_reset(FILE* stream)
+{
+#if USE_FILENO && USE_ISATTY
+ if (isatty(fileno(stream)) && terminal_supports_color()) {
+ fprintf(stream, "\033[0m");
+ fflush(stream);
+ }
+#endif
+}
+
+static const char* const log_level_strings[] = {"emergengy",
+ "alert",
+ "critical",
+ "error",
+ "warning",
+ "note",
+ "info",
+ "debug"};
+
+SERD_CONST_FUNC
+SerdStatus
+serd_quiet_error_func(void* handle, const SerdLogEntry* entry)
+{
+ (void)handle;
+ (void)entry;
+ return SERD_SUCCESS;
+}
+
+static const char*
+get_log_field(const SerdLogField* const fields,
+ const size_t n_fields,
+ const char* const key)
+{
+ for (size_t i = 0; i < n_fields; ++i) {
+ if (!strcmp(fields[i].key, key)) {
+ return fields[i].value;
+ }
+ }
+
+ return NULL;
+}
+
+const char*
+serd_log_entry_get_field(const SerdLogEntry* const entry, const char* const key)
+{
+ return get_log_field(entry->fields, entry->n_fields, key);
+}
+
SerdStatus
-serd_world_error(const SerdWorld* world, const SerdError* e)
+serd_world_vlogf(const SerdWorld* world,
+ const char* domain,
+ SerdLogLevel level,
+ const size_t n_fields,
+ const SerdLogField* fields,
+ const char* fmt,
+ va_list args)
{
- if (world->error_func) {
- world->error_func(world->error_handle, e);
+ // Copy args (which may be an array) to portably get a pointer
+ va_list ap;
+ va_copy(ap, args);
+
+ const SerdLogEntry e = {domain, fields, fmt, &ap, level, n_fields};
+ SerdStatus st = SERD_SUCCESS;
+
+ if (world->log_func) {
+ st = world->log_func(world->log_handle, &e);
} else {
- fprintf(stderr, "error: ");
- if (e->cursor) {
- fprintf(stderr,
- "%s:%u:%u: ",
- serd_node_string(e->cursor->file),
- e->cursor->line,
- e->cursor->col);
+ // Print input file and position prefix if available
+ const char* const file = serd_log_entry_get_field(&e, "SERD_FILE");
+ const char* const line = serd_log_entry_get_field(&e, "SERD_LINE");
+ const char* const col = serd_log_entry_get_field(&e, "SERD_COL");
+ if (file && line && col) {
+ serd_ansi_start(stderr, 1, true);
+ fprintf(stderr, "%s:%s:%s: ", file, line, col);
+ serd_ansi_reset(stderr);
}
- vfprintf(stderr, e->fmt, *e->args);
+
+ // Print GCC-style level prefix (error, warning, etc)
+ serd_ansi_start(stderr, level_color(level), true);
+ fprintf(stderr, "%s: ", log_level_strings[level]);
+ serd_ansi_reset(stderr);
+
+ // Using a copy isn't necessary here, but it avoids a clang-tidy bug
+ vfprintf(stderr, fmt, ap);
+ }
+
+ va_end(ap);
+ return st;
+}
+
+SerdStatus
+serd_world_logf(const SerdWorld* world,
+ const char* domain,
+ SerdLogLevel level,
+ const size_t n_fields,
+ const SerdLogField* fields,
+ const char* fmt,
+ ...)
+{
+ va_list args;
+ va_start(args, fmt);
+
+ const SerdStatus st =
+ serd_world_vlogf(world, domain, level, n_fields, fields, fmt, args);
+
+ va_end(args);
+ return st;
+}
+
+SerdStatus
+serd_world_vlogf_internal(const SerdWorld* world,
+ SerdStatus st,
+ SerdLogLevel level,
+ const SerdCursor* cursor,
+ const char* fmt,
+ va_list args)
+{
+ char st_str[12];
+ snprintf(st_str, sizeof(st_str), "%u", st);
+ if (cursor) {
+ const char* file = serd_node_string(serd_cursor_name(cursor));
+
+ char line[24];
+ snprintf(line, sizeof(line), "%u", serd_cursor_line(cursor));
+
+ char col[24];
+ snprintf(col, sizeof(col), "%u", serd_cursor_column(cursor));
+
+ const SerdLogField fields[] = {{"SERD_STATUS", st_str},
+ {"SERD_FILE", file},
+ {"SERD_LINE", line},
+ {"SERD_COL", col}};
+
+ serd_world_vlogf(world, "serd", level, 4, fields, fmt, args);
+ } else {
+ const SerdLogField fields[] = {{"SERD_STATUS", st_str}};
+ serd_world_vlogf(world, "serd", level, 1, fields, fmt, args);
}
- return e->status;
+
+ return st;
}
SerdStatus
-serd_world_errorf(const SerdWorld* world, SerdStatus st, const char* fmt, ...)
+serd_world_logf_internal(const SerdWorld* world,
+ SerdStatus st,
+ SerdLogLevel level,
+ const SerdCursor* cursor,
+ const char* fmt,
+ ...)
{
va_list args;
va_start(args, fmt);
- const SerdError e = {st, NULL, fmt, &args};
- serd_world_error(world, &e);
+ serd_world_vlogf_internal(world, st, level, cursor, fmt, args);
va_end(args);
return st;
}
@@ -118,10 +290,8 @@ serd_world_get_blank(SerdWorld* world)
}
void
-serd_world_set_error_func(SerdWorld* world,
- SerdErrorFunc error_func,
- void* handle)
+serd_world_set_log_func(SerdWorld* world, SerdLogFunc log_func, void* handle)
{
- world->error_func = error_func;
- world->error_handle = handle;
+ world->log_func = log_func;
+ world->log_handle = handle;
}
diff --git a/src/world.h b/src/world.h
index f76b9483..7a4a129d 100644
--- a/src/world.h
+++ b/src/world.h
@@ -19,13 +19,14 @@
#include "serd/serd.h"
+#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
struct SerdWorldImpl {
SerdNodes* nodes;
- SerdErrorFunc error_func;
- void* error_handle;
+ SerdLogFunc log_func;
+ void* log_handle;
SerdNode* blank_node;
const SerdNode* rdf_first;
const SerdNode* rdf_nil;
@@ -38,10 +39,33 @@ struct SerdWorldImpl {
uint32_t next_blank_id;
};
+/// Write a message to the log
+SERD_API
+SERD_LOG_FUNC(5, 0)
SerdStatus
-serd_world_error(const SerdWorld* world, const SerdError* e);
+serd_world_vlogf_internal(const SerdWorld* world,
+ SerdStatus st,
+ SerdLogLevel level,
+ const SerdCursor* cursor,
+ const char* fmt,
+ va_list args);
+/// Write a message to the log
+SERD_API
+SERD_LOG_FUNC(5, 6)
SerdStatus
-serd_world_errorf(const SerdWorld* world, SerdStatus st, const char* fmt, ...);
+serd_world_logf_internal(const SerdWorld* world,
+ SerdStatus st,
+ SerdLogLevel level,
+ const SerdCursor* cursor,
+ const char* fmt,
+ ...);
+
+#define SERD_LOG_ERRORF(world, st, fmt, ...) \
+ serd_world_logf_internal( \
+ world, st, SERD_LOG_LEVEL_ERR, NULL, fmt, __VA_ARGS__)
+
+#define SERD_LOG_ERROR(world, st, msg) \
+ serd_world_logf_internal(world, st, SERD_LOG_LEVEL_ERR, NULL, msg)
#endif // SERD_WORLD_H
diff --git a/src/writer.c b/src/writer.c
index f55b47d4..d15320ae 100644
--- a/src/writer.c
+++ b/src/writer.c
@@ -135,8 +135,8 @@ struct SerdWriterImpl {
WriteContext* anon_stack;
size_t anon_stack_size;
SerdByteSink* byte_sink;
- SerdErrorFunc error_func;
- void* error_handle;
+ SerdLogFunc log_func;
+ void* log_handle;
WriteContext context;
char* bprefix;
size_t bprefix_len;
@@ -237,12 +237,12 @@ sink(const void* buf, size_t len, SerdWriter* writer)
const size_t written = serd_byte_sink_write(buf, len, writer->byte_sink);
if (written != len) {
if (errno) {
- serd_world_errorf(writer->world,
- SERD_ERR_BAD_WRITE,
- "write error (%s)\n",
- strerror(errno));
+ SERD_LOG_ERRORF(writer->world,
+ SERD_ERR_BAD_WRITE,
+ "write error (%s)\n",
+ strerror(errno));
} else {
- serd_world_errorf(writer->world, SERD_ERR_BAD_WRITE, "write error\n");
+ SERD_LOG_ERROR(writer->world, SERD_ERR_BAD_WRITE, "write error\n");
}
}
@@ -267,7 +267,7 @@ write_character(SerdWriter* writer,
const uint32_t c = parse_utf8_char(utf8, size);
switch (*size) {
case 0:
- serd_world_errorf(
+ SERD_LOG_ERRORF(
writer->world, SERD_ERR_BAD_ARG, "invalid UTF-8 start: %X\n", utf8[0]);
*st = SERD_ERR_BAD_TEXT;
return 0;
@@ -767,10 +767,10 @@ write_uri_node(SerdWriter* const writer,
if (!has_scheme && !supports_uriref(writer) &&
!serd_env_base_uri(writer->env)) {
- serd_world_errorf(writer->world,
- SERD_ERR_BAD_ARG,
- "syntax does not support URI reference <%s>\n",
- node_str);
+ SERD_LOG_ERRORF(writer->world,
+ SERD_ERR_BAD_ARG,
+ "syntax does not support URI reference <%s>\n",
+ node_str);
return SERD_ERR_BAD_ARG;
}
@@ -817,10 +817,10 @@ write_curie(SerdWriter* const writer,
case SERD_NTRIPLES:
case SERD_NQUADS:
if ((st = serd_env_expand_in_place(writer->env, node, &prefix, &suffix))) {
- serd_world_errorf(writer->world,
- st,
- "undefined namespace prefix `%s'\n",
- serd_node_string(node));
+ SERD_LOG_ERRORF(writer->world,
+ st,
+ "undefined namespace prefix in `%s'\n",
+ serd_node_string(node));
return st;
}
TRY(st, esink("<", 1, writer));
@@ -1113,10 +1113,10 @@ serd_writer_end_anon(SerdWriter* writer, const SerdNode* node)
}
if (writer->anon_stack_size == 0) {
- return serd_world_errorf(writer->world,
- SERD_ERR_UNKNOWN,
- "unexpected end of anonymous node `%s'\n",
- serd_node_string(node));
+ return SERD_LOG_ERRORF(writer->world,
+ SERD_ERR_UNKNOWN,
+ "unexpected end of anonymous node `%s'\n",
+ serd_node_string(node));
}
SerdStatus st = write_sep(writer, writer->context.flags, SEP_ANON_END);