diff --git a/include/private/target/stream.h b/include/private/target/stream.h index 1c9f7f7f8..dc068e534 100644 --- a/include/private/target/stream.h +++ b/include/private/target/stream.h @@ -22,6 +22,7 @@ # include # include # include +# include # include "private/config/wrapper/thread_safety.h" /** @@ -30,6 +31,8 @@ struct stream_target { /** The stream this target writes to. */ FILE *stream; +/** ANSI colors for different severities (when using ansi terminal) */ + char escape_codes[8][32]; # ifdef STUMPLESS_THREAD_SAFETY_SUPPORTED /** * Protects stream. This mutex must be locked by a thread before it can write @@ -61,6 +64,7 @@ new_stream_target( FILE *stream ); int sendto_stream_target( struct stream_target *target, const char *msg, - size_t msg_length ); + size_t msg_length, + const struct stumpless_entry *entry ); #endif /* __STUMPLESS_PRIVATE_TARGET_STREAM_H */ diff --git a/include/stumpless/severity.h b/include/stumpless/severity.h index a3d1693f4..eb1517ebe 100644 --- a/include/stumpless/severity.h +++ b/include/stumpless/severity.h @@ -190,6 +190,20 @@ enum stumpless_severity { STUMPLESS_FOREACH_SEVERITY( STUMPLESS_GENERATE_ENUM ) }; +/** + * The default stumpless ansi terminal color values + */ +#define STUMPLESS_SEVERITY_EMERG_DEFAULT_COLOR "\33[31;1m" +#define STUMPLESS_SEVERITY_ALERT_DEFAULT_COLOR "\33[31;1m" +#define STUMPLESS_SEVERITY_CRIT_DEFAULT_COLOR "\33[31m" +#define STUMPLESS_SEVERITY_ERR_DEFAULT_COLOR "\33[31m" +#define STUMPLESS_SEVERITY_WARNING_DEFAULT_COLOR "\33[33m" +#define STUMPLESS_SEVERITY_NOTICE_DEFAULT_COLOR "\33[32m" +#define STUMPLESS_SEVERITY_INFO_DEFAULT_COLOR "\33[37m" +#define STUMPLESS_SEVERITY_DEBUG_DEFAULT_COLOR "\33[0m" + + + /** * Equivalent to the DEBUG severity. Trace level messages include extra * information, but do not have a distinct severity value in log entries. diff --git a/include/stumpless/target/stream.h b/include/stumpless/target/stream.h index ef14320b1..4a4b32b28 100644 --- a/include/stumpless/target/stream.h +++ b/include/stumpless/target/stream.h @@ -38,6 +38,7 @@ # include # include # include +# include # ifdef __cplusplus extern "C" { @@ -154,6 +155,34 @@ STUMPLESS_PUBLIC_FUNCTION struct stumpless_target * stumpless_open_stream_target( const char *name, FILE *stream ); +/** + * Sets the ANSI escape code (https://en.wikipedia.org/wiki/ANSI_escape_code) to + * be printed in a specific target when a log is made at some severity. + * + * It should be used with stdout or stderr as targets, since it is only for aesthetic purposes. + * + * **Thread Safety: MT-Safe race:escape_code** + * This function is thread safe, of course assuming that escape_code is not modified by + * any other threads during execution. + * + * **Async Signal Safety: AS-Unsafe lock** + * This function is not safe to call from signal handlers due to the destruction + * of a lock that may be in use. + * + * **Async Cancel Safety: AC-Unsafe lock** + * This function is not safe to call from threads that may be asynchronously + * cancelled, as the cleanup of the lock may not be completed. + * + * @param target The name of the target for which to set the colors. + * + * @param severity The severity code (LOG_ERR etc.) to which we set the specific color. + * + * @param escape_code The ANSI escape code representing the color. + */ +STUMPLESS_PUBLIC_FUNCTION +void +stumpless_set_severity_color( struct stumpless_target *target, enum stumpless_severity severity, const char *escape_code ); + # ifdef __cplusplus } /* extern "C" */ # endif diff --git a/src/target.c b/src/target.c index d06c76791..3cf78a023 100644 --- a/src/target.c +++ b/src/target.c @@ -267,7 +267,7 @@ stumpless_add_entry( struct stumpless_target *target, break; case STUMPLESS_STREAM_TARGET: - result = sendto_stream_target( target->id, buffer, builder_length ); + result = sendto_stream_target( target->id, buffer, builder_length, entry ); break; case STUMPLESS_WINDOWS_EVENT_LOG_TARGET: diff --git a/src/target/stream.c b/src/target/stream.c index 0fe23a77d..97a1e48f4 100644 --- a/src/target/stream.c +++ b/src/target/stream.c @@ -18,7 +18,10 @@ #include #include +#include #include +#include +#include #include #include "private/config/wrapper/locale.h" #include "private/config/wrapper/thread_safety.h" @@ -28,6 +31,7 @@ #include "private/target.h" #include "private/target/stream.h" #include "private/validate.h" +#include "private/severity.h" void stumpless_close_stream_target( const struct stumpless_target *target ) { @@ -48,12 +52,39 @@ stumpless_close_stream_target( const struct stumpless_target *target ) { struct stumpless_target * stumpless_open_stderr_target( const char *name ) { - return stumpless_open_stream_target( name, stderr ); + struct stumpless_target *target = stumpless_open_stream_target( name, stderr ); + + if (target == NULL) return NULL; + + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_EMERG_VALUE , STUMPLESS_SEVERITY_EMERG_DEFAULT_COLOR ); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_ALERT_VALUE , STUMPLESS_SEVERITY_ALERT_DEFAULT_COLOR ); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_CRIT_VALUE , STUMPLESS_SEVERITY_CRIT_DEFAULT_COLOR ); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_ERR_VALUE , STUMPLESS_SEVERITY_ERR_DEFAULT_COLOR ); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_WARNING_VALUE, STUMPLESS_SEVERITY_WARNING_DEFAULT_COLOR); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_NOTICE_VALUE , STUMPLESS_SEVERITY_NOTICE_DEFAULT_COLOR ); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_INFO_VALUE , STUMPLESS_SEVERITY_INFO_DEFAULT_COLOR ); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_DEBUG_VALUE , STUMPLESS_SEVERITY_DEBUG_DEFAULT_COLOR ); + + return target; } struct stumpless_target * stumpless_open_stdout_target( const char *name ) { - return stumpless_open_stream_target( name, stdout ); + struct stumpless_target *target = stumpless_open_stream_target( name, stdout ); + + if (target == NULL) return NULL; + + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_EMERG_VALUE , STUMPLESS_SEVERITY_EMERG_DEFAULT_COLOR ); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_ALERT_VALUE , STUMPLESS_SEVERITY_ALERT_DEFAULT_COLOR ); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_CRIT_VALUE , STUMPLESS_SEVERITY_CRIT_DEFAULT_COLOR ); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_ERR_VALUE , STUMPLESS_SEVERITY_ERR_DEFAULT_COLOR ); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_WARNING_VALUE, STUMPLESS_SEVERITY_WARNING_DEFAULT_COLOR); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_NOTICE_VALUE , STUMPLESS_SEVERITY_NOTICE_DEFAULT_COLOR ); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_INFO_VALUE , STUMPLESS_SEVERITY_INFO_DEFAULT_COLOR ); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_DEBUG_VALUE , STUMPLESS_SEVERITY_DEBUG_DEFAULT_COLOR ); + + + return target; } struct stumpless_target * @@ -76,6 +107,9 @@ stumpless_open_stream_target( const char *name, FILE *stream ) { goto fail_id; } + for (unsigned short i = 0; i < 8; i++) + ((struct stream_target *)(target->id))->escape_codes[i][0] = 0; + stumpless_set_current_target( target ); return target; @@ -85,6 +119,30 @@ stumpless_open_stream_target( const char *name, FILE *stream ) { return NULL; } +void +stumpless_set_severity_color( struct stumpless_target *target, enum stumpless_severity severity, const char *escape_code ) +{ + if (severity_is_invalid(severity)) { + raise_invalid_severity(severity); + return; + } + + lock_target(target); + if (target->type != STUMPLESS_STREAM_TARGET) { + raise_target_unsupported("This function is only supported for stream targets"); + return; + } + + struct stream_target *starget = (struct stream_target *)target->id; + + strncpy(starget->escape_codes[severity], escape_code, 32); + starget->escape_codes[severity][31] = 0; + + unlock_target(target); + clear_error(); +} + + /* private definitions */ void @@ -111,20 +169,44 @@ new_stream_target( FILE *stream ) { int sendto_stream_target( struct stream_target *target, const char *msg, - size_t msg_length ) { + size_t msg_length, + const struct stumpless_entry *entry ) { size_t fwrite_result; - + enum stumpless_severity severity = stumpless_get_entry_severity(entry); + const char *sev_code = target->escape_codes[severity]; + unsigned short sev_code_len = strlen(sev_code); + const char *reset_code = "\33[0m"; + unsigned short reset_code_len = strlen(reset_code); + config_lock_mutex( &target->stream_mutex ); - fwrite_result = fwrite( msg, sizeof( char ), msg_length, target->stream ); - config_unlock_mutex( &target->stream_mutex ); + if (sev_code_len != 0) + { + fwrite_result = fwrite( sev_code, sizeof( char ), sev_code_len, target->stream ); + if( fwrite_result != sev_code_len ) { + goto write_failure; + } + } + + fwrite_result = fwrite( msg, sizeof( char ), msg_length, target->stream ); if( fwrite_result != msg_length ) { goto write_failure; } + if (sev_code_len != 0) + { + fwrite_result = fwrite( reset_code, sizeof( char ), reset_code_len, target->stream ); + + if( fwrite_result != reset_code_len ) { + goto write_failure; + } + } + + config_unlock_mutex( &target->stream_mutex ); return cap_size_t_to_int( fwrite_result + 1 ); write_failure: + config_unlock_mutex( &target->stream_mutex ); raise_stream_write_failure( ); return -1; } diff --git a/src/windows/stumpless.def b/src/windows/stumpless.def index b47062f1d..3a1c10c37 100644 --- a/src/windows/stumpless.def +++ b/src/windows/stumpless.def @@ -246,3 +246,4 @@ EXPORTS stumpless_set_entry_message_str_w @229 stumpless_get_prival_string @230 + stumpless_set_severity_color @231 diff --git a/test/function/target/stream.cpp b/test/function/target/stream.cpp index 37b12d765..986cfc64e 100644 --- a/test/function/target/stream.cpp +++ b/test/function/target/stream.cpp @@ -23,6 +23,8 @@ #include #include #include +#include + #include "test/helper/assert.hpp" #include "test/helper/fixture.hpp" #include "test/helper/rfc5424.hpp" @@ -248,6 +250,143 @@ namespace { EXPECT_ERROR_ID_EQ( STUMPLESS_ARGUMENT_EMPTY ); } + TEST( StreamTargetStderrTest, ColoredStream ) { + struct stumpless_target *target; + size_t i; + const char *filename = "streamtargetcoloredstderrtest.log"; + + target = stumpless_open_stderr_target( "stderr-target" ); + ASSERT_NOT_NULL( target ); +#ifndef STDERR_FILENO + int save_stderr = _dup(_fileno(stderr)); +#else + int save_stderr = dup(STDERR_FILENO); +#endif + for (i = 0; i < 8; i++) + { +#ifndef STDERR_FILENO + save_stderr = _dup(save_stderr); +#else + save_stderr = dup(save_stderr); +#endif + freopen(filename, "a+", stderr); + + stumpless_add_log_str(target, i, stumpless_get_severity_string((enum stumpless_severity)i)); + + +#ifndef STDERR_FILENO + _dup2(save_stderr, _fileno(stderr)); +#else + fclose(stderr); + stderr = fdopen(save_stderr, "w"); +#endif + } + + stumpless_close_target(target); + stumpless_free_all(); + + std::ifstream infile(filename); +/* + * Google Test uses a more simplified regex engine for windows + * (see here: https://google.github.io/googletest/advanced.html#regular-expression-syntax) + * which does not support things like conditionals, grouping or brackets. + * Hence we use the following preprocessor statement to use a different + * test when this is run on platforms with the simple regex, and hopefully + * once a reasonably complex regex engine is implemented in all the major + * platforms this is not needed. + **/ +#ifdef GTEST_USES_SIMPLE_RE + std::string line; + bool first = true; + + while (std::getline(infile, line)) + { + EXPECT_THAT(line, testing::Conditional( + first, + testing::AnyOf( + testing::MatchesRegex("\33\\[3\\d;?1?m.*"), + testing::MatchesRegex("\33\\[0m.*") + ), + testing::AnyOf( + testing::MatchesRegex("\33\\[0m\33\\[3\\d;?1?m.*"), + testing::MatchesRegex("\33\\[0m\33\\[0m.*"), + testing::MatchesRegex("\33\\[0m.*") + ) + )); + if (first) first = false; + } +#else + std::stringstream buf; + buf << infile.rdbuf(); + std::string src = buf.str(); + EXPECT_THAT(src, testing::MatchesRegex("(\33\\[(0|3[0-7]);?1?m.*\n\33\\[0m)*")); +#endif + } + + TEST( StreamTargetStdoutTest, ColoredStream ) { + struct stumpless_target *target; + size_t i; + const char *filename = "streamtargetcoloredstdouttest.log"; + + target = stumpless_open_stdout_target( "stdout-target" ); + ASSERT_NOT_NULL( target ); +#ifndef STDOUT_FILENO + int save_stdout = _dup(_fileno(stdout)); +#else + int save_stdout = dup(STDOUT_FILENO); +#endif + for (i = 0; i < 8; i++) + { +#ifndef STDOUT_FILENO + save_stdout = _dup(save_stdout); +#else + save_stdout = dup(save_stdout); +#endif + freopen(filename, "a+", stdout); + + stumpless_add_log_str(target, i, stumpless_get_severity_string((enum stumpless_severity)i)); + +#ifndef STDOUT_FILENO + _dup2(save_stdout, _fileno(stdout)); +#else + fclose(stdout); + stdout = fdopen(save_stdout, "w"); +#endif + } + + stumpless_close_target(target); + stumpless_free_all(); + + std::ifstream infile(filename); +/* See comment in StreamTargetStderrTest.ColoredStream */ +#ifdef GTEST_USES_SIMPLE_RE + std::string line; + bool first = true; + + while (std::getline(infile, line)) + { + EXPECT_THAT(line, testing::Conditional( + first, + testing::AnyOf( + testing::MatchesRegex("\33\\[3\\d;?1?m.*"), + testing::MatchesRegex("\33\\[0m.*") + ), + testing::AnyOf( + testing::MatchesRegex("\33\\[0m\33\\[3\\d;?1?m.*"), + testing::MatchesRegex("\33\\[0m\33\\[0m.*"), + testing::MatchesRegex("\33\\[0m.*") + ) + )); + if (first) first = false; + } +#else + std::stringstream buf; + buf << infile.rdbuf(); + std::string src = buf.str(); + EXPECT_THAT(src, testing::MatchesRegex("(\33\\[(0|3[0-7]);?1?m.*\n\33\\[0m)*")); +#endif + } + TEST( StreamTargetWriteTest, ReadOnlyStream ) { struct stumpless_target *target; struct stumpless_entry *basic_entry; @@ -277,4 +416,25 @@ namespace { remove( filename ); } + + TEST(StreamSetSeverityColorTest, InvalidSeverity) { + struct stumpless_target *target = stumpless_open_stdout_target("stdout"); + stumpless_set_severity_color(target, (enum stumpless_severity) 15, "\33[0m"); + + EXPECT_ERROR_ID_EQ(STUMPLESS_INVALID_SEVERITY); + + stumpless_close_target(target); + stumpless_free_all(); + } + + TEST(StreamSetSeverityColorTest, WrongTargetType) { + char buf; + struct stumpless_target *target = stumpless_open_buffer_target("buffer", &buf, 1); + stumpless_set_severity_color(target, STUMPLESS_SEVERITY_ALERT, "\33[0m"); + + EXPECT_ERROR_ID_EQ(STUMPLESS_TARGET_UNSUPPORTED); + + stumpless_close_target(target); + stumpless_free_all(); + } }