Coverage Report

Created: 2025-12-31 06:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/suricata7/src/output-json-stats.c
Line
Count
Source
1
/* Copyright (C) 2014-2024 Open Information Security Foundation
2
 *
3
 * You can copy, redistribute or modify this Program under the terms of
4
 * the GNU General Public License version 2 as published by the Free
5
 * Software Foundation.
6
 *
7
 * This program is distributed in the hope that it will be useful,
8
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10
 * GNU General Public License for more details.
11
 *
12
 * You should have received a copy of the GNU General Public License
13
 * version 2 along with this program; if not, write to the Free Software
14
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
15
 * 02110-1301, USA.
16
 */
17
18
/**
19
 * \file
20
 *
21
 * \author Tom DeCanio <td@npulsetech.com>
22
 *
23
 * Implements JSON stats counters logging portion of the engine.
24
 */
25
26
#include "suricata-common.h"
27
#include "detect.h"
28
#include "pkt-var.h"
29
#include "conf.h"
30
#include "detect-engine.h"
31
32
#include "threads.h"
33
#include "threadvars.h"
34
#include "tm-threads.h"
35
36
#include "util-print.h"
37
#include "util-time.h"
38
#include "util-unittest.h"
39
#include "util-validate.h"
40
41
#include "util-debug.h"
42
#include "output.h"
43
#include "util-privs.h"
44
#include "util-buffer.h"
45
46
#include "util-logopenfile.h"
47
48
#include "output-json.h"
49
#include "output-json-stats.h"
50
51
71
#define MODULE_NAME "JsonStatsLog"
52
53
extern bool stats_decoder_events;
54
extern const char *stats_decoder_events_prefix;
55
56
/**
57
 * specify which engine info will be printed in stats log.
58
 * ALL means both last reload and ruleset stats.
59
 */
60
typedef enum OutputEngineInfo_ {
61
    OUTPUT_ENGINE_LAST_RELOAD = 0,
62
    OUTPUT_ENGINE_RULESET,
63
    OUTPUT_ENGINE_ALL,
64
} OutputEngineInfo;
65
66
typedef struct OutputStatsCtx_ {
67
    LogFileCtx *file_ctx;
68
    uint8_t flags; /** Store mode */
69
} OutputStatsCtx;
70
71
typedef struct JsonStatsLogThread_ {
72
    OutputStatsCtx *statslog_ctx;
73
    LogFileCtx *file_ctx;
74
    MemBuffer *buffer;
75
} JsonStatsLogThread;
76
77
static json_t *EngineStats2Json(const DetectEngineCtx *de_ctx,
78
                                const OutputEngineInfo output)
79
0
{
80
0
    char timebuf[64];
81
0
    const SigFileLoaderStat *sig_stat = NULL;
82
83
0
    json_t *jdata = json_object();
84
0
    if (jdata == NULL) {
85
0
        return NULL;
86
0
    }
87
88
0
    if (output == OUTPUT_ENGINE_LAST_RELOAD || output == OUTPUT_ENGINE_ALL) {
89
0
        SCTime_t last_reload = SCTIME_FROM_TIMEVAL(&de_ctx->last_reload);
90
0
        CreateIsoTimeString(last_reload, timebuf, sizeof(timebuf));
91
0
        json_object_set_new(jdata, "last_reload", json_string(timebuf));
92
0
    }
93
94
0
    sig_stat = &de_ctx->sig_stat;
95
0
    if ((output == OUTPUT_ENGINE_RULESET || output == OUTPUT_ENGINE_ALL) &&
96
0
        sig_stat != NULL)
97
0
    {
98
0
        json_object_set_new(jdata, "rules_loaded",
99
0
                            json_integer(sig_stat->good_sigs_total));
100
0
        json_object_set_new(jdata, "rules_failed",
101
0
                            json_integer(sig_stat->bad_sigs_total));
102
0
        json_object_set_new(jdata, "rules_skipped", json_integer(sig_stat->skipped_sigs_total));
103
0
    }
104
105
0
    return jdata;
106
0
}
107
108
static TmEcode OutputEngineStats2Json(json_t **jdata, const OutputEngineInfo output)
109
0
{
110
0
    DetectEngineCtx *de_ctx = DetectEngineGetCurrent();
111
0
    if (de_ctx == NULL) {
112
0
        goto err1;
113
0
    }
114
    /* Since we need to deference de_ctx pointer, we don't want to lost it. */
115
0
    DetectEngineCtx *list = de_ctx;
116
117
0
    json_t *js_tenant_list = json_array();
118
0
    json_t *js_tenant = NULL;
119
120
0
    if (js_tenant_list == NULL) {
121
0
        goto err2;
122
0
    }
123
124
0
    while(list) {
125
0
        js_tenant = json_object();
126
0
        if (js_tenant == NULL) {
127
0
            goto err3;
128
0
        }
129
0
        json_object_set_new(js_tenant, "id", json_integer(list->tenant_id));
130
131
0
        json_t *js_stats = EngineStats2Json(list, output);
132
0
        if (js_stats == NULL) {
133
0
            goto err4;
134
0
        }
135
0
        json_object_update(js_tenant, js_stats);
136
0
        json_array_append_new(js_tenant_list, js_tenant);
137
0
        json_decref(js_stats);
138
0
        list = list->next;
139
0
    }
140
141
0
    DetectEngineDeReference(&de_ctx);
142
0
    *jdata = js_tenant_list;
143
0
    return TM_ECODE_OK;
144
145
0
err4:
146
0
    json_object_clear(js_tenant);
147
0
    json_decref(js_tenant);
148
149
0
err3:
150
0
    json_object_clear(js_tenant_list);
151
0
    json_decref(js_tenant_list);
152
153
0
err2:
154
0
    DetectEngineDeReference(&de_ctx);
155
156
0
err1:
157
0
    json_object_set_new(*jdata, "message", json_string("Unable to get info"));
158
0
    return TM_ECODE_FAILED;
159
0
}
160
161
0
TmEcode OutputEngineStatsReloadTime(json_t **jdata) {
162
0
    return OutputEngineStats2Json(jdata, OUTPUT_ENGINE_LAST_RELOAD);
163
0
}
164
165
0
TmEcode OutputEngineStatsRuleset(json_t **jdata) {
166
0
    return OutputEngineStats2Json(jdata, OUTPUT_ENGINE_RULESET);
167
0
}
168
169
static json_t *OutputStats2Json(json_t *js, const char *key)
170
0
{
171
0
    void *iter;
172
173
0
    const char *dot = strchr(key, '.');
174
0
    if (dot == NULL)
175
0
        return NULL;
176
0
    if (strlen(dot) > 2) {
177
0
        if (*(dot + 1) == '.' && *(dot + 2) != '\0')
178
0
            dot = strchr(dot + 2, '.');
179
0
    }
180
181
0
    size_t predot_len = (dot - key) + 1;
182
0
    char s[predot_len];
183
0
    strlcpy(s, key, predot_len);
184
185
0
    iter = json_object_iter_at(js, s);
186
0
    const char *s2 = strchr(dot+1, '.');
187
188
0
    json_t *value = json_object_iter_value(iter);
189
0
    if (value == NULL) {
190
0
        value = json_object();
191
192
0
        if (!strncmp(s, "detect", 6)) {
193
0
            json_t *js_engine = NULL;
194
195
0
            TmEcode ret = OutputEngineStats2Json(&js_engine, OUTPUT_ENGINE_ALL);
196
0
            if (ret == TM_ECODE_OK && js_engine) {
197
0
                json_object_set_new(value, "engines", js_engine);
198
0
            }
199
0
        }
200
0
        json_object_set_new(js, s, value);
201
0
    }
202
0
    if (s2 != NULL) {
203
0
        return OutputStats2Json(value, &key[dot-key+1]);
204
0
    }
205
0
    return value;
206
0
}
207
208
/** \brief turn StatsTable into a json object
209
 *  \param flags JSON_STATS_* flags for controlling output
210
 */
211
json_t *StatsToJSON(const StatsTable *st, uint8_t flags)
212
0
{
213
0
    const char delta_suffix[] = "_delta";
214
0
    struct timeval tval;
215
0
    gettimeofday(&tval, NULL);
216
217
0
    json_t *js_stats = json_object();
218
0
    if (unlikely(js_stats == NULL)) {
219
0
        return NULL;
220
0
    }
221
222
    /* Uptime, in seconds. */
223
0
    double up_time_d = difftime(tval.tv_sec, st->start_time);
224
0
    json_object_set_new(js_stats, "uptime",
225
0
        json_integer((int)up_time_d));
226
227
0
    uint32_t u = 0;
228
0
    if (flags & JSON_STATS_TOTALS) {
229
0
        for (u = 0; u < st->nstats; u++) {
230
0
            if (st->stats[u].name == NULL)
231
0
                continue;
232
0
            json_t *js_type = NULL;
233
0
            const char *stat_name = st->stats[u].short_name;
234
            /*
235
             * When there's no short-name, the stat is added to
236
             * the "global" stats namespace, just like "uptime"
237
             */
238
0
            if (st->stats[u].short_name == NULL) {
239
0
                stat_name = st->stats[u].name;
240
0
                js_type = js_stats;
241
0
            } else {
242
0
                js_type = OutputStats2Json(js_stats, st->stats[u].name);
243
0
            }
244
0
            if (js_type != NULL) {
245
0
                json_object_set_new(js_type, stat_name, json_integer(st->stats[u].value));
246
247
0
                if (flags & JSON_STATS_DELTAS) {
248
0
                    char deltaname[strlen(stat_name) + strlen(delta_suffix) + 1];
249
0
                    snprintf(deltaname, sizeof(deltaname), "%s%s", stat_name, delta_suffix);
250
0
                    json_object_set_new(js_type, deltaname,
251
0
                        json_integer(st->stats[u].value - st->stats[u].pvalue));
252
0
                }
253
0
            }
254
0
        }
255
0
    }
256
257
    /* per thread stats - stored in a "threads" object. */
258
0
    if (st->tstats != NULL && (flags & JSON_STATS_THREADS)) {
259
        /* for each thread (store) */
260
0
        json_t *threads = json_object();
261
0
        if (unlikely(threads == NULL)) {
262
0
            json_decref(js_stats);
263
0
            return NULL;
264
0
        }
265
0
        uint32_t x;
266
0
        for (x = 0; x < st->ntstats; x++) {
267
0
            uint32_t offset = x * st->nstats;
268
0
            const char *tm_name = NULL;
269
0
            json_t *thread = NULL;
270
271
            /* for each counter */
272
0
            for (u = offset; u < (offset + st->nstats); u++) {
273
0
                if (st->tstats[u].name == NULL)
274
0
                    continue;
275
276
0
                DEBUG_VALIDATE_BUG_ON(st->tstats[u].tm_name == NULL);
277
278
0
                if (tm_name == NULL) {
279
                    // First time we see a set tm_name. Remember it
280
                    // and allocate the stats object for this thread.
281
0
                    tm_name = st->tstats[u].tm_name;
282
0
                    thread = json_object();
283
0
                    if (unlikely(thread == NULL)) {
284
0
                        json_decref(js_stats);
285
0
                        json_decref(threads);
286
0
                        return NULL;
287
0
                    }
288
0
                } else {
289
0
                    DEBUG_VALIDATE_BUG_ON(strcmp(tm_name, st->tstats[u].tm_name) != 0);
290
0
                    DEBUG_VALIDATE_BUG_ON(thread == NULL);
291
0
                }
292
293
0
                json_t *js_type = NULL;
294
0
                const char *stat_name = st->tstats[u].short_name;
295
0
                if (st->tstats[u].short_name == NULL) {
296
0
                    stat_name = st->tstats[u].name;
297
0
                    js_type = threads;
298
0
                } else {
299
0
                    js_type = OutputStats2Json(thread, st->tstats[u].name);
300
0
                }
301
302
0
                if (js_type != NULL) {
303
0
                    json_object_set_new(js_type, stat_name, json_integer(st->tstats[u].value));
304
305
0
                    if (flags & JSON_STATS_DELTAS) {
306
0
                        char deltaname[strlen(stat_name) + strlen(delta_suffix) + 1];
307
0
                        snprintf(deltaname, sizeof(deltaname), "%s%s", stat_name, delta_suffix);
308
0
                        json_object_set_new(js_type, deltaname,
309
0
                            json_integer(st->tstats[u].value - st->tstats[u].pvalue));
310
0
                    }
311
0
                }
312
0
            }
313
0
            if (tm_name != NULL) {
314
0
                DEBUG_VALIDATE_BUG_ON(thread == NULL);
315
0
                json_object_set_new(threads, tm_name, thread);
316
0
            }
317
0
        }
318
0
        json_object_set_new(js_stats, "threads", threads);
319
0
    }
320
0
    return js_stats;
321
0
}
322
323
static int JsonStatsLogger(ThreadVars *tv, void *thread_data, const StatsTable *st)
324
0
{
325
0
    SCEnter();
326
0
    JsonStatsLogThread *aft = (JsonStatsLogThread *)thread_data;
327
328
0
    struct timeval tval;
329
0
    gettimeofday(&tval, NULL);
330
331
0
    json_t *js = json_object();
332
0
    if (unlikely(js == NULL))
333
0
        return 0;
334
0
    char timebuf[64];
335
0
    CreateIsoTimeString(SCTIME_FROM_TIMEVAL(&tval), timebuf, sizeof(timebuf));
336
0
    json_object_set_new(js, "timestamp", json_string(timebuf));
337
0
    json_object_set_new(js, "event_type", json_string("stats"));
338
339
0
    json_t *js_stats = StatsToJSON(st, aft->statslog_ctx->flags);
340
0
    if (js_stats == NULL) {
341
0
        json_decref(js);
342
0
        return 0;
343
0
    }
344
345
0
    json_object_set_new(js, "stats", js_stats);
346
347
0
    OutputJSONBuffer(js, aft->file_ctx, &aft->buffer);
348
0
    MemBufferReset(aft->buffer);
349
350
0
    json_object_clear(js_stats);
351
0
    json_object_del(js, "stats");
352
0
    json_object_clear(js);
353
0
    json_decref(js);
354
355
0
    SCReturnInt(0);
356
0
}
357
358
static TmEcode JsonStatsLogThreadInit(ThreadVars *t, const void *initdata, void **data)
359
0
{
360
0
    JsonStatsLogThread *aft = SCCalloc(1, sizeof(JsonStatsLogThread));
361
0
    if (unlikely(aft == NULL))
362
0
        return TM_ECODE_FAILED;
363
364
0
    if(initdata == NULL)
365
0
    {
366
0
        SCLogDebug("Error getting context for EveLogStats.  \"initdata\" argument NULL");
367
0
        goto error_exit;
368
0
    }
369
370
0
    aft->buffer = MemBufferCreateNew(JSON_OUTPUT_BUFFER_SIZE);
371
0
    if (aft->buffer == NULL) {
372
0
        goto error_exit;
373
0
    }
374
375
    /* Use the Output Context (file pointer and mutex) */
376
0
    aft->statslog_ctx = ((OutputCtx *)initdata)->data;
377
378
0
    aft->file_ctx = LogFileEnsureExists(aft->statslog_ctx->file_ctx);
379
0
    if (!aft->file_ctx) {
380
0
        goto error_exit;
381
0
    }
382
383
0
    *data = (void *)aft;
384
0
    return TM_ECODE_OK;
385
386
0
error_exit:
387
0
    if (aft->buffer != NULL) {
388
0
        MemBufferFree(aft->buffer);
389
0
    }
390
0
    SCFree(aft);
391
0
    return TM_ECODE_FAILED;
392
0
}
393
394
static TmEcode JsonStatsLogThreadDeinit(ThreadVars *t, void *data)
395
0
{
396
0
    JsonStatsLogThread *aft = (JsonStatsLogThread *)data;
397
0
    if (aft == NULL) {
398
0
        return TM_ECODE_OK;
399
0
    }
400
401
0
    MemBufferFree(aft->buffer);
402
403
    /* clear memory */
404
0
    memset(aft, 0, sizeof(JsonStatsLogThread));
405
406
0
    SCFree(aft);
407
0
    return TM_ECODE_OK;
408
0
}
409
410
static void OutputStatsLogDeinitSub(OutputCtx *output_ctx)
411
0
{
412
0
    OutputStatsCtx *stats_ctx = output_ctx->data;
413
0
    SCFree(stats_ctx);
414
0
    SCFree(output_ctx);
415
0
}
416
417
static OutputInitResult OutputStatsLogInitSub(ConfNode *conf, OutputCtx *parent_ctx)
418
0
{
419
0
    OutputInitResult result = { NULL, false };
420
0
    OutputJsonCtx *ajt = parent_ctx->data;
421
422
0
    if (!StatsEnabled()) {
423
0
        SCLogError("eve.stats: stats are disabled globally: set stats.enabled to true. "
424
0
                   "See %s/configuration/suricata-yaml.html#stats",
425
0
                GetDocURL());
426
0
        return result;
427
0
    }
428
429
0
    OutputStatsCtx *stats_ctx = SCMalloc(sizeof(OutputStatsCtx));
430
0
    if (unlikely(stats_ctx == NULL))
431
0
        return result;
432
433
0
    if (stats_decoder_events &&
434
0
            strcmp(stats_decoder_events_prefix, "decoder") == 0) {
435
0
        SCLogWarning("eve.stats will not display "
436
0
                     "all decoder events correctly. See ticket #2225. Set a prefix in "
437
0
                     "stats.decoder-events-prefix.");
438
0
    }
439
440
0
    stats_ctx->flags = JSON_STATS_TOTALS;
441
442
0
    if (conf != NULL) {
443
0
        const char *totals = ConfNodeLookupChildValue(conf, "totals");
444
0
        const char *threads = ConfNodeLookupChildValue(conf, "threads");
445
0
        const char *deltas = ConfNodeLookupChildValue(conf, "deltas");
446
0
        SCLogDebug("totals %s threads %s deltas %s", totals, threads, deltas);
447
448
0
        if ((totals != NULL && ConfValIsFalse(totals)) &&
449
0
                (threads != NULL && ConfValIsFalse(threads))) {
450
0
            SCFree(stats_ctx);
451
0
            SCLogError("Cannot disable both totals and threads in stats logging");
452
0
            return result;
453
0
        }
454
455
0
        if (totals != NULL && ConfValIsFalse(totals)) {
456
0
            stats_ctx->flags &= ~JSON_STATS_TOTALS;
457
0
        }
458
0
        if (threads != NULL && ConfValIsTrue(threads)) {
459
0
            stats_ctx->flags |= JSON_STATS_THREADS;
460
0
        }
461
0
        if (deltas != NULL && ConfValIsTrue(deltas)) {
462
0
            stats_ctx->flags |= JSON_STATS_DELTAS;
463
0
        }
464
0
        SCLogDebug("stats_ctx->flags %08x", stats_ctx->flags);
465
0
    }
466
467
0
    OutputCtx *output_ctx = SCCalloc(1, sizeof(OutputCtx));
468
0
    if (unlikely(output_ctx == NULL)) {
469
0
        SCFree(stats_ctx);
470
0
        return result;
471
0
    }
472
473
0
    SCLogDebug("Preparing file context for stats submodule logger");
474
    /* Share output slot with thread 1 */
475
0
    stats_ctx->file_ctx = LogFileEnsureExists(ajt->file_ctx);
476
0
    if (!stats_ctx->file_ctx) {
477
0
        SCFree(stats_ctx);
478
0
        SCFree(output_ctx);
479
0
        return result;
480
0
    }
481
482
0
    output_ctx->data = stats_ctx;
483
0
    output_ctx->DeInit = OutputStatsLogDeinitSub;
484
485
0
    result.ctx = output_ctx;
486
0
    result.ok = true;
487
0
    return result;
488
0
}
489
490
71
void JsonStatsLogRegister(void) {
491
    /* register as child of eve-log */
492
71
    OutputRegisterStatsSubModule(LOGGER_JSON_STATS, "eve-log", MODULE_NAME,
493
71
        "eve-log.stats", OutputStatsLogInitSub, JsonStatsLogger,
494
        JsonStatsLogThreadInit, JsonStatsLogThreadDeinit, NULL);
495
71
}
496
497
#ifdef UNITTESTS
498
#include "tests/output-json-stats.c"
499
#endif