Introduction
I’ve been working on a project, and I needed to add logging to the app. I
wanted a log formatting function that can take a prefix and a format string
(think a sprintf
-type format string but safer). This function needs to be
more than just an sprintf
knockoff; it also needs to prefix the date and time
to the message. Oh, the messages are passed in without a new line, so we need
to add that too.
The Log Function
The log function needs to be smart so just wrapping vasprintf isn’t good enough.
char *str_log(const char *prefix, const char *fmt, ...)
{
char *msg = NULL;
char myfmt[512];
char *myfmt_a = NULL;
char *myfmt_ptr;
size_t myfmt_len;
char mytime[26];
va_list ap;
time_t now;
int ret;
if (fmt == NULL)
return NULL;
if (prefix == NULL)
prefix = "";
now = time(NULL);
memcpy(mytime, ctime(&now), sizeof(mytime));
mytime[24] = '\0';
myfmt_len = strlen(mytime) + 2 + strlen(prefix) + 2 + strlen(fmt) + 2;
if (myfmt_len < sizeof(myfmt)) {
myfmt_ptr = myfmt;
} else {
myfmt_a = malloc(myfmt_len);
myfmt_a[myfmt_len-1] = '\0';
myfmt_ptr = myfmt_a;
}
snprintf(myfmt_ptr, myfmt_len, "%s %s%s%s%s%s\n", mytime,
*prefix=='\0'?"":"\"", prefix, *prefix=='\0'?"":"\"",
*prefix=='\0'?"":" ",
fmt);
va_start(ap, fmt);
ret = vasprintf(&msg, myfmt_ptr, ap);
if (myfmt_a != NULL)
free(myfmt_a);
va_end(ap);
if (ret == -1)
return NULL;
return msg;
}
This log function meets all the requirements: time, prefix, format, arguments.
It works by first creating a string with the time and prefix. Then it puts that
plus the format passed into the function into a buffer. Finally, our new format
string and the other arguments are passed into vasprintf
, which handles
putting the full message together. This implementation is optimized for a short
format but can take one of any length.
Let’s break down how it works piece by piece.
if (fmt == NULL)
return NULL;
No format means we don’t have anything to log.
if (prefix == NULL)
prefix = "";
prefix
will be used with functions like strlen
and, as we know, strlen
gets crash happy when it’s passed NULL. We could check if prefix
is NULL
before each time we use it, but that’s tedious. Let’s set it to ""
so it’s
always safe to use.
now = time(NULL);
memcpy(mytime, ctime(&now), sizeof(mytime));
Now we generate the time string using ctime
. We have to use an intermediate
time variable with ctime
because it takes a pointer. It would be nice if we
could pass the time in directly but alas, that’s not possible.
The buffer being used is 26 bytes because ctime
will always return a 26-byte
string: 24 bytes for the data, 1 byte for a trailing ‘\n’, and finally 1 bytes
for the NULL terminator. If we can use ctime_r
we should use it because it is
thread-safe. It also saves us from needing to use memcpy
. We’d end up with:
ctime_r(&now, mytime);
instead.
mytime[24] = '\0';
We don’t want the newline that ctime
outputs because we want the time part
of the entry to be on the same line as the rest of the message. The newline
will always be at index 24, so kill it.
myfmt_len = strlen(mytime) + 2 + strlen(prefix) + 2 + strlen(fmt) + 2;
Now we determine the length of the format string with the time and prefix prepended. The fixed numbers (three 2s, totaling 6) are there for the prepend formatting. The +6 is needed to account for:
- Space after time.
- Quote before prefix
- Quote after prefix
- Space after prefix.
- New line at end.
- NULL terminator.
if (myfmt_len < sizeof(myfmt)) {
myfmt_ptr = myfmt;
} else {
myfmt_a = malloc(myfmt_len);
myfmt_a[myfmt_len-1] = '\0';
myfmt_ptr = myfmt_a;
}
First, check if the format we’re building will fit into a stack buffer. This is
a small optimization to prevent a malloc
every time this function is called.
If the new format is larger than the buffer, we need to malloc
. myfmt_ptr
is used to track which buffer is being used.
snprintf(myfmt_ptr, myfmt_len, "%s %s%s%s%s%s\n", mytime,
*prefix=='\0'?"":"\"", prefix, *prefix=='\0'?"":"\"",
*prefix=='\0'?"":" ",
fmt);
Put together the new format, which is ’time “prefix” fmt’. If set, the prefix will have quotes around it. If empty, the quotes will be omitted.
va_start(ap, fmt);
ret = vasprintf(&msg, myfmt_ptr, ap);
if (myfmt_a != NULL)
free(myfmt_a);
va_end(ap);
Since we can have all kinds of different length inputs we can’t use a fixed
size static buffer. There is no possible way to know how much data will be
logged. Instead, vasprintf
is used because it will return (in an out
parameter) an allocated string.
if (ret == -1)
return NULL;
return msg;
All that’s left is for us to check whether the string was created successfully and return it so it can be passed on for logging. Be it to a file or something like syslog.
Testing
Lets write a simple test app and give this log function a try.
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
int main(int argc, char **argv)
{
char *l;
l = str_log("abc", "123");
printf("%s", l);
free(l);
l = str_log(NULL, "xyz");
printf("%s", l);
free(l);
l = str_log("ALL", "%d: %d: %s", 4, -1, "message");
printf("%s", l);
free(l);
return 0;
}
Mon Jan 21 19:52:23 2019 "abc" 123
Mon Jan 21 19:52:23 2019 xyz
Mon Jan 21 19:52:23 2019 "ALL" 4: -1: message
The output looks like what we’d expect to see.
Other Thoughts
This logging function takes a string prefix, which doesn’t make much sense when
you’re using a format string because you could make the prefix part of the
message. What makes more sense is to take an enum
value of log levels and
have the level as a string added to the message. All we’d need is an enum
to
string function and treat just like we do the prefix in this function.