/* vim: set expandtab ts=4 sw=4: */
/*
* You may redistribute this program and/or modify it under the terms of
* the GNU General Public License as published by the Free Software Foundation,
* either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
// needed for String_vprintf()
#include
#include "admin/Admin.h"
#include "admin/AdminLog.h"
#include "benc/Dict.h"
#include "benc/String.h"
#include "crypto/random/Random.h"
#include "io/Writer.h"
#include "memory/BufferAllocator.h"
#include "util/log/Log.h"
#include "util/log/Log_impl.h"
#include "util/Hex.h"
#define string_strcmp
#define string_strrchr
#define string_strlen
#include "util/platform/libc/string.h"
#include
#include
#include
#include
#define MAX_SUBSCRIPTIONS 64
#define FILE_NAME_COUNT 32
struct Subscription
{
/** The log level to match against, all higher levels will also be matched. */
enum Log_Level level;
/**
* true if the file name is the internal name
* which can be compared using a pointer equality check
*/
bool internalName : 1;
/** The line number within the file or 0 to match all lines. */
int lineNum : 31;
/** The name of the file to match against or null to match any file. */
const char* file;
/** The transaction ID of the message which solicited this stream of logs. */
String* txid;
/** A hopefully unique (random) number identifying this stream. */
uint8_t streamId[8];
/** An allocator which will live during the lifecycle of the Subscription */
struct Allocator* alloc;
};
struct AdminLog
{
struct Log pub;
struct Subscription subscriptions[MAX_SUBSCRIPTIONS];
uint32_t subscriptionCount;
const char* fileNames[FILE_NAME_COUNT];
struct Admin* admin;
struct Allocator* alloc;
struct Random* rand;
};
static inline const char* getShortName(const char* fullFilePath)
{
const char* out = strrchr(fullFilePath, '/');
if (out) {
return out + 1;
}
return fullFilePath;
}
static inline bool isMatch(struct Subscription* subscription,
struct AdminLog* logger,
enum Log_Level logLevel,
const char* file,
int line)
{
if (subscription->file) {
if (subscription->internalName) {
if (file != subscription->file) {
return false;
}
} else {
const char* shortFileName = getShortName(file);
if (strcmp(shortFileName, subscription->file)) {
return false;
}
// It's the same name but so we'll swap the name for the internal name and then
// it can be compared quickly with a pointer comparison.
subscription->file = shortFileName;
subscription->internalName = true;
for (int i = 0; i < FILE_NAME_COUNT; i++) {
if (logger->fileNames[i] == shortFileName) {
break;
}
if (logger->fileNames[i] == NULL) {
logger->fileNames[i] = shortFileName;
logger->fileNames[(i + 1) % FILE_NAME_COUNT] = NULL;
break;
}
}
}
}
if (logLevel < subscription->level) {
return false;
}
if (subscription->lineNum && line != subscription->lineNum) {
return false;
}
return true;
}
static Dict* makeLogMessage(struct Subscription* subscription,
struct AdminLog* logger,
enum Log_Level logLevel,
const char* fullFilePath,
uint32_t line,
const char* format,
va_list vaArgs,
struct Allocator* alloc)
{
time_t now;
time(&now);
Dict* out = Dict_new(alloc);
char* buff = Allocator_malloc(alloc, 20);
Hex_encode((uint8_t*)buff, 20, subscription->streamId, 8);
Dict_putString(out, String_new("streamId", alloc), String_new(buff, alloc), alloc);
Dict_putInt(out, String_new("time", alloc), now, alloc);
Dict_putString(out,
String_new("level", alloc),
String_new(Log_nameForLevel(logLevel), alloc),
alloc);
const char* shortName = getShortName(fullFilePath);
Dict_putString(out, String_new("file", alloc), String_new((char*)shortName, alloc), alloc);
Dict_putInt(out, String_new("line", alloc), line, alloc);
String* message = String_vprintf(alloc, format, vaArgs);
// Strip all of the annoying \n marks in the log entries.
if (message->len > 0 && message->bytes[message->len - 1] == '\n') {
message->len--;
}
Dict_putString(out, String_new("message", alloc), message, alloc);
return out;
}
static void removeSubscription(struct AdminLog* log, struct Subscription* sub)
{
Allocator_free(sub->alloc);
log->subscriptionCount--;
if (log->subscriptionCount == 0 || sub == &log->subscriptions[log->subscriptionCount]) {
return;
}
Bits_memcpyConst(sub,
&log->subscriptions[log->subscriptionCount],
sizeof(struct Subscription));
}
static void doLog(struct Log* genericLog,
enum Log_Level logLevel,
const char* fullFilePath,
int line,
const char* format,
va_list args)
{
struct AdminLog* log = (struct AdminLog*) genericLog;
Dict* message = NULL;
#define ALLOC_BUFFER_SZ 4096
uint8_t allocBuffer[ALLOC_BUFFER_SZ];
for (int i = 0; i < (int)log->subscriptionCount; i++) {
if (isMatch(&log->subscriptions[i], log, logLevel, fullFilePath, line)) {
if (!message) {
struct Allocator* alloc = BufferAllocator_new(allocBuffer, ALLOC_BUFFER_SZ);
message = makeLogMessage(&log->subscriptions[i],
log,
logLevel,
fullFilePath,
line,
format,
args,
alloc);
}
int ret = Admin_sendMessage(message, log->subscriptions[i].txid, log->admin);
if (ret) {
removeSubscription(log, &log->subscriptions[i]);
}
}
}
}
static void subscribe(Dict* args, void* vcontext, String* txid)
{
struct AdminLog* log = (struct AdminLog*) vcontext;
String* levelName = Dict_getString(args, String_CONST("level"));
enum Log_Level level = (levelName) ? Log_levelForName(levelName->bytes) : Log_Level_DEBUG;
int64_t* lineNumPtr = Dict_getInt(args, String_CONST("line"));
String* fileStr = Dict_getString(args, String_CONST("file"));
const char* file = (fileStr && fileStr->len > 0) ? fileStr->bytes : NULL;
char* error = "2+2=5";
if (level == Log_Level_INVALID) {
level = Log_Level_KEYS;
}
if (lineNumPtr && *lineNumPtr < 0) {
error = "Invalid line number, must be positive or 0 to signify any line is acceptable.";
} else if (log->subscriptionCount >= MAX_SUBSCRIPTIONS) {
error = "Max subscription count reached.";
} else {
struct Subscription* sub = &log->subscriptions[log->subscriptionCount];
sub->level = level;
sub->alloc = Allocator_child(log->alloc);
if (file) {
int i;
for (i = 0; i < FILE_NAME_COUNT; i++) {
if (log->fileNames[i] && !strcmp(log->fileNames[i], file)) {
file = log->fileNames[i];
sub->internalName = true;
break;
}
}
if (i == FILE_NAME_COUNT) {
file = String_new(file, sub->alloc)->bytes;
sub->internalName = false;
}
}
sub->file = file;
sub->lineNum = (lineNumPtr) ? *lineNumPtr : 0;
sub->txid = String_clone(txid, sub->alloc);
Random_bytes(log->rand, (uint8_t*) sub->streamId, 8);
uint8_t streamIdHex[20];
Hex_encode(streamIdHex, 20, sub->streamId, 8);
Dict response = Dict_CONST(
String_CONST("error"), String_OBJ(String_CONST("none")), Dict_CONST(
String_CONST("streamId"), String_OBJ(String_CONST((char*)streamIdHex)), NULL
));
Admin_sendMessage(&response, txid, log->admin);
log->subscriptionCount++;
return;
}
Dict response = Dict_CONST(
String_CONST("error"), String_OBJ(String_CONST(error)), NULL
);
Admin_sendMessage(&response, txid, log->admin);
}
static void unsubscribe(Dict* args, void* vcontext, String* txid)
{
struct AdminLog* log = (struct AdminLog*) vcontext;
String* streamIdHex = Dict_getString(args, String_CONST("streamId"));
uint8_t streamId[8];
char* error = NULL;
if (streamIdHex->len != 16 || Hex_decode(streamId, 8, (uint8_t*)streamIdHex->bytes, 16) != 8) {
error = "Invalid streamId.";
} else {
error = "No such subscription.";
for (int i = 0; i < (int)log->subscriptionCount; i++) {
if (!Bits_memcmp(streamId, log->subscriptions[i].streamId, 8)) {
removeSubscription(log, &log->subscriptions[i]);
error = "none";
break;
}
}
}
Dict response = Dict_CONST(
String_CONST("error"), String_OBJ(String_CONST(error)), NULL
);
Admin_sendMessage(&response, txid, log->admin);
}
struct Log* AdminLog_registerNew(struct Admin* admin, struct Allocator* alloc, struct Random* rand)
{
struct AdminLog* log = Allocator_clone(alloc, (&(struct AdminLog) {
.pub = {
.print = doLog
},
.admin = admin,
.alloc = alloc,
.rand = rand
}));
Admin_registerFunction("AdminLog_subscribe", subscribe, log, true,
((struct Admin_FunctionArg[]) {
{ .name = "level", .required = 0, .type = "String" },
{ .name = "line", .required = 0, .type = "Int" },
{ .name = "file", .required = 0, .type = "String" }
}), admin);
Admin_registerFunction("AdminLog_unsubscribe", unsubscribe, log, true,
((struct Admin_FunctionArg[]) {
{ .name = "streamId", .required = 1, .type = "String" }
}), admin);
return &log->pub;
}