new exit cause: shutdown timeout (#19903)
* added new exit cause: shutdown timeout to daemon status and sentry * added to exit cause * increase status file version to 20 * tested; exit timeout now works independently of sentry * added timezone to status file * added agent_version_now when posting crash reports * added cloud provider, cloud instance and cloud region to status file and sentry
Costa Tsaousis committed
Mar 19, 2025 at 19:11 UTC
6001d73c17498f89b2c34da9ce33b41ee1823696
8 files changed
+155
-26
src/daemon/daemon-shutdown-watcher.c
+9
-7
@@ -3,6 +3,10 @@
3
#include "daemon-shutdown-watcher.h"
4
#include "daemon-status-file.h"
5
6
+#ifdef ENABLE_SENTRY
7
+#include "sentry-native/sentry-native.h"
8
+#endif
9
+
10
watcher_step_t *watcher_steps;
11
12
static struct completion shutdown_begin_completion;
@@ -12,6 +16,10 @@ static ND_THREAD *watcher_thread;
16
NEVER_INLINE
17
static void shutdown_timed_out(void) {
18
// keep this as a separate function, to have it logged like this in sentry
19
+ daemon_status_file_shutdown_timeout();
20
+#ifdef ENABLE_SENTRY
21
+ nd_sentry_add_shutdown_timeout_as_breadcrumb();
22
+#endif
23
abort();
24
}
25
@@ -48,7 +56,6 @@ static void watcher_wait_for_step(const watcher_step_id_t step_id, usec_t shutdo
56
57
daemon_status_file_shutdown_step(watcher_steps[step_id].msg);
58
51
-#ifdef ENABLE_SENTRY
59
// Wait with a timeout
60
time_t timeout = 135; // systemd gives us 150, we timeout at 135
61
@@ -57,11 +64,6 @@ static void watcher_wait_for_step(const watcher_step_id_t step_id, usec_t shutdo
64
remaining_seconds = 0;
65
66
bool ok = completion_timedwait_for(&watcher_steps[step_id].p, remaining_seconds);
60
-#else
61
- // Wait indefinitely
62
- bool ok = true;
63
- completion_wait_for(&watcher_steps[step_id].p);
64
-#endif
67
68
usec_t step_duration = now_monotonic_usec() - step_start_time;
69
@@ -191,7 +193,7 @@ void watcher_thread_start() {
193
completion_init(&shutdown_begin_completion);
194
completion_init(&shutdown_end_completion);
195
194
- watcher_thread = nd_thread_create("P[WATCHER]", NETDATA_THREAD_OPTION_JOINABLE, watcher_main, NULL);
196
+ watcher_thread = nd_thread_create("EXIT_WATCHER", NETDATA_THREAD_OPTION_JOINABLE, watcher_main, NULL);
197
}
198
199
void watcher_thread_stop() {
src/daemon/daemon-status-file.c
+88
-17
@@ -211,6 +211,13 @@ static void daemon_status_file_to_json(BUFFER *wb, DAEMON_STATUS_FILE *ds) {
211
buffer_json_member_add_string_or_empty(wb, "container", ds->container);
212
buffer_json_member_add_time_t(wb, "uptime", ds->boottime);
213
214
+ if(ds->v >= 20) {
215
+ buffer_json_member_add_string_or_empty(wb, "timezone", ds->timezone);
216
+ buffer_json_member_add_string_or_empty(wb, "cloud_provider", ds->cloud_provider_type);
217
+ buffer_json_member_add_string_or_empty(wb, "cloud_instance", ds->cloud_instance_type);
218
+ buffer_json_member_add_string_or_empty(wb, "cloud_region", ds->cloud_instance_region);
219
+ }
220
+
221
buffer_json_member_add_object(wb, "boot");
222
{
223
buffer_json_member_add_uuid_compact(wb, "id", ds->boot_id.uuid);
@@ -323,6 +330,7 @@ static bool daemon_status_file_from_json(json_object *jobj, void *data, BUFFER *
330
bool required_v16 = version >= 16 ? strict : false;
331
bool required_v17 = version >= 17 ? strict : false;
332
bool required_v18 = version >= 18 ? strict : false;
333
+ bool required_v20 = version >= 20 ? strict : false;
334
335
// Parse timestamp
336
JSONC_PARSE_TXT2CHAR_OR_ERROR_AND_RETURN(jobj, path, "@timestamp", datetime, error, required_v1);
@@ -419,6 +427,13 @@ static bool daemon_status_file_from_json(json_object *jobj, void *data, BUFFER *
427
ds->var_cache = OS_SYSTEM_DISK_SPACE_EMPTY;
428
});
429
});
430
+
431
+ if(version >= 20) {
432
+ JSONC_PARSE_TXT2CHAR_OR_ERROR_AND_RETURN(jobj, path, "timezone", ds->timezone, error, required_v20);
433
+ JSONC_PARSE_TXT2CHAR_OR_ERROR_AND_RETURN(jobj, path, "cloud_provider", ds->cloud_provider_type, error, required_v20);
434
+ JSONC_PARSE_TXT2CHAR_OR_ERROR_AND_RETURN(jobj, path, "cloud_instance", ds->cloud_instance_type, error, required_v20);
435
+ JSONC_PARSE_TXT2CHAR_OR_ERROR_AND_RETURN(jobj, path, "cloud_region", ds->cloud_instance_region, error, required_v20);
436
+ }
437
});
438
439
// Parse os object
@@ -539,6 +554,10 @@ static void daemon_status_file_migrate_once(void) {
554
strncpyz(session_status.os_version, last_session_status.os_version, sizeof(session_status.os_version) - 1);
555
strncpyz(session_status.os_id, last_session_status.os_id, sizeof(session_status.os_id) - 1);
556
strncpyz(session_status.os_id_like, last_session_status.os_id_like, sizeof(session_status.os_id_like) - 1);
557
+ strncpyz(session_status.timezone, last_session_status.timezone, sizeof(session_status.timezone) - 1);
558
+ strncpyz(session_status.cloud_provider_type, last_session_status.cloud_provider_type, sizeof(session_status.cloud_provider_type) - 1);
559
+ strncpyz(session_status.cloud_instance_type, last_session_status.cloud_instance_type, sizeof(session_status.cloud_instance_type) - 1);
560
+ strncpyz(session_status.cloud_instance_region, last_session_status.cloud_instance_region, sizeof(session_status.cloud_instance_region) - 1);
561
562
session_status.restarts = last_session_status.restarts + 1;
563
session_status.reliability = last_session_status.reliability;
@@ -612,6 +631,9 @@ static void daemon_status_file_refresh(DAEMON_STATUS status) {
631
632
get_daemon_status_fields_from_system_info(&session_status);
633
634
+ if(netdata_configured_timezone)
635
+ strncpyz(session_status.timezone, netdata_configured_timezone, sizeof(session_status.timezone) - 1);
636
+
637
session_status.exit_reason = exit_initiated_get();
638
session_status.profile = nd_profile_detect_and_configure(false);
639
@@ -954,10 +976,11 @@ struct post_status_file_thread_data {
976
void post_status_file(struct post_status_file_thread_data *d) {
977
CLEAN_BUFFER *wb = buffer_create(0, NULL);
978
buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
957
- buffer_json_member_add_string(wb, "exit_cause", d->cause); // custom
958
- buffer_json_member_add_string(wb, "message", d->msg); // ECS
959
- buffer_json_member_add_uint64(wb, "priority", d->priority); // custom
960
- buffer_json_member_add_uint64(wb, "version_saved", d->status->v); // custom
979
+ buffer_json_member_add_string(wb, "exit_cause", d->cause);
980
+ buffer_json_member_add_string(wb, "message", d->msg);
981
+ buffer_json_member_add_uint64(wb, "priority", d->priority);
982
+ buffer_json_member_add_uint64(wb, "version_saved", d->status->v);
983
+ buffer_json_member_add_string(wb, "agent_version_now", NETDATA_VERSION);
984
daemon_status_file_to_json(wb, d->status);
985
buffer_json_finalize(wb);
986
@@ -1157,6 +1180,11 @@ void daemon_status_file_check_crash(void) {
1180
msg = "Netdata was last crashed while exiting after receiving a deadly signal";
1181
pri = PRI_DEADLY_SIGNAL;
1182
}
1183
+ else if(last_session_status.exit_reason & EXIT_REASON_SHUTDOWN_TIMEOUT) {
1184
+ cause = "exit timeout";
1185
+ msg = "Netdata was last killed because it couldn't shutdown on time";
1186
+ pri = PRI_FATAL;
1187
+ }
1188
else if(last_session_status.exit_reason != EXIT_REASON_NONE &&
1189
!is_exit_reason_normal(last_session_status.exit_reason)) {
1190
cause = "fatal on exit";
@@ -1425,6 +1453,46 @@ bool daemon_status_file_deadly_signal_received(EXIT_REASON reason, SIGNAL_CODE c
1453
return duplicate;
1454
}
1455
1456
+// --------------------------------------------------------------------------------------------------------------------
1457
+// shutdown related functions
1458
+
1459
+static SPINLOCK shutdown_timeout_spinlock = SPINLOCK_INITIALIZER;
1460
+
1461
+void daemon_status_file_shutdown_timeout(void) {
1462
+ FUNCTION_RUN_ONCE();
1463
+
1464
+ spinlock_lock(&shutdown_timeout_spinlock);
1465
+
1466
+ dsf_acquire(session_status);
1467
+ exit_initiated_add(EXIT_REASON_SHUTDOWN_TIMEOUT);
1468
+ session_status.exit_reason |= EXIT_REASON_SHUTDOWN_TIMEOUT;
1469
+ dsf_release(session_status);
1470
+
1471
+ strncpyz(session_status.fatal.function, "shutdown_timeout", sizeof(session_status.fatal.function) - 1);
1472
+
1473
+ CLEAN_BUFFER *wb = buffer_create(0, NULL);
1474
+ daemon_status_file_save(wb, &session_status, false);
1475
+
1476
+ // keep the spinlock locked, to prevent further steps updating the status
1477
+}
1478
+
1479
+void daemon_status_file_shutdown_step(const char *step) {
1480
+ if(session_status.fatal.filename[0] || !spinlock_trylock(&shutdown_timeout_spinlock))
1481
+ // we have a fatal logged
1482
+ return;
1483
+
1484
+ if(step != NULL)
1485
+ snprintfz(session_status.fatal.function, sizeof(session_status.fatal.function), "shutdown(%s)", step);
1486
+ else
1487
+ session_status.fatal.function[0] = '\0';
1488
+
1489
+ daemon_status_file_update_status(DAEMON_STATUS_EXITING);
1490
+
1491
+ spinlock_unlock(&shutdown_timeout_spinlock);
1492
+}
1493
+
1494
+// --------------------------------------------------------------------------------------------------------------------
1495
+
1496
bool daemon_status_file_has_last_crashed(void) {
1497
return last_session_status.status != DAEMON_STATUS_EXITED || !is_exit_reason_normal(last_session_status.exit_reason);
1498
}
@@ -1449,19 +1517,6 @@ void daemon_status_file_startup_step(const char *step) {
1517
daemon_status_file_update_status(DAEMON_STATUS_INITIALIZING);
1518
}
1519
1452
-void daemon_status_file_shutdown_step(const char *step) {
1453
- if(session_status.fatal.filename[0])
1454
- // we have a fatal logged
1455
- return;
1456
-
1457
- if(step != NULL)
1458
- snprintfz(session_status.fatal.function, sizeof(session_status.fatal.function), "shutdown(%s)", step);
1459
- else
1460
- session_status.fatal.function[0] = '\0';
1461
-
1462
- daemon_status_file_update_status(DAEMON_STATUS_EXITING);
1463
-}
1464
-
1520
// --------------------------------------------------------------------------------------------------------------------
1521
// public API to get values
1522
@@ -1497,6 +1552,22 @@ const char *daemon_status_file_get_os_id_like(void) {
1552
return session_status.os_id_like;
1553
}
1554
1555
+const char *daemon_status_file_get_cloud_provider_type(void) {
1556
+ return session_status.cloud_provider_type;
1557
+}
1558
+
1559
+const char *daemon_status_file_get_cloud_instance_type(void) {
1560
+ return session_status.cloud_instance_type;
1561
+}
1562
+
1563
+const char *daemon_status_file_get_cloud_instance_region(void) {
1564
+ return session_status.cloud_instance_region;
1565
+}
1566
+
1567
+const char *daemon_status_file_get_timezone(void) {
1568
+ return session_status.timezone;
1569
+}
1570
+
1571
const char *daemon_status_file_get_fatal_filename(void) {
1572
return session_status.fatal.filename;
1573
}
src/daemon/daemon-status-file.h
+11
-2
@@ -7,7 +7,7 @@
7
#include "daemon/config/netdata-conf-profile.h"
8
#include "database/rrd-database-mode.h"
9
10
-#define STATUS_FILE_VERSION 19
10
+#define STATUS_FILE_VERSION 20
11
12
typedef enum {
13
DAEMON_STATUS_NONE,
@@ -73,6 +73,10 @@ typedef struct daemon_status_file {
73
char os_version[32]; // ECS: os.version
74
char os_id[64]; // ECS: os.family
75
char os_id_like[64]; // ECS: os.platform
76
+ char timezone[32];
77
+ char cloud_provider_type[32];
78
+ char cloud_instance_type[32];
79
+ char cloud_instance_region[32];
80
bool read_system_info;
81
82
char stack_traces[15]; // the backend for capturing stack traces
@@ -115,6 +119,7 @@ bool daemon_status_file_was_incomplete_shutdown(void);
119
120
void daemon_status_file_startup_step(const char *step);
121
void daemon_status_file_shutdown_step(const char *step);
122
+void daemon_status_file_shutdown_timeout(void);
123
124
void daemon_status_file_init(void);
125
void daemon_status_file_register_fatal(const char *filename, const char *function, const char *message, const char *errno_str, const char *stack_trace, long line);
@@ -127,8 +132,12 @@ const char *daemon_status_file_get_os_name(void);
132
const char *daemon_status_file_get_os_version(void);
133
const char *daemon_status_file_get_os_id(void);
134
const char *daemon_status_file_get_os_id_like(void);
135
+const char *daemon_status_file_get_timezone(void);
136
+const char *daemon_status_file_get_cloud_provider_type(void);
137
+const char *daemon_status_file_get_cloud_instance_type(void);
138
+const char *daemon_status_file_get_cloud_instance_region(void);
139
131
-const char *daemon_status_file_get_fatal_filename(void);
140
+ const char *daemon_status_file_get_fatal_filename(void);
141
const char *daemon_status_file_get_fatal_function(void);
142
const char *daemon_status_file_get_fatal_message(void);
143
const char *daemon_status_file_get_fatal_errno(void);
src/daemon/sentry-native/sentry-native.c
+32
@@ -161,6 +161,10 @@ void nd_sentry_init(void) {
161
nd_sentry_set_tag("os_version", daemon_status_file_get_os_version());
162
nd_sentry_set_tag("os_id", daemon_status_file_get_os_id());
163
nd_sentry_set_tag("os_id_like", daemon_status_file_get_os_id_like());
164
+ nd_sentry_set_tag("cloud_provider", daemon_status_file_get_cloud_provider_type());
165
+ nd_sentry_set_tag("cloud_type", daemon_status_file_get_cloud_instance_type());
166
+ nd_sentry_set_tag("cloud_region", daemon_status_file_get_cloud_instance_region());
167
+ nd_sentry_set_tag("timezone", daemon_status_file_get_timezone());
168
169
// profile
170
CLEAN_BUFFER *profile = buffer_create(0, NULL);
@@ -262,3 +266,31 @@ void nd_sentry_add_fatal_message_as_breadcrumb(void) {
266
sentry_value_set_by_key(crumb, "data", data);
267
sentry_add_breadcrumb(crumb);
268
}
269
+
270
+void nd_sentry_add_shutdown_timeout_as_breadcrumb(void) {
271
+ if (!analytics_check_enabled())
272
+ return;
273
+
274
+ const char *function = "shutdown_timeout";
275
+ strncpyz(g_sentry_event_message, function, sizeof(g_sentry_event_message) - 1);
276
+
277
+ nd_sentry_set_tag_uptime();
278
+
279
+ // Set the transaction name to the function where the error occurred
280
+ // this should be low cardinality
281
+ sentry_set_transaction(function);
282
+
283
+ // Set the fingerprint to the function where the error occurred
284
+ sentry_set_fingerprint("{{ default }}", function, NULL);
285
+
286
+ sentry_value_t crumb = sentry_value_new_breadcrumb("fatal", "shutdown_timeout() event details");
287
+
288
+ sentry_value_t data = sentry_value_new_object();
289
+ nd_sentry_add_key_value_charp(data, "function", function);
290
+ nd_sentry_add_key_value_charp(data, "thread", nd_thread_tag());
291
+ nd_sentry_add_key_value_uint64(data, "thread_id", gettid_cached());
292
+ nd_sentry_add_key_value_charp(data, "status", DAEMON_STATUS_2str(daemon_status_file_get_status()));
293
+
294
+ sentry_value_set_by_key(crumb, "data", data);
295
+ sentry_add_breadcrumb(crumb);
296
+}
src/daemon/sentry-native/sentry-native.h
+1
@@ -12,6 +12,7 @@ const char *nd_sentry_path(void);
12
13
void nd_sentry_set_user(const char *guid);
14
void nd_sentry_add_fatal_message_as_breadcrumb(void);
15
+void nd_sentry_add_shutdown_timeout_as_breadcrumb(void);
16
17
void nd_sentry_crash_report(bool enable);
18
src/database/rrdhost-system-info.c
+9
@@ -659,5 +659,14 @@ void get_daemon_status_fields_from_system_info(DAEMON_STATUS_FILE *ds) {
659
ds->kubernetes = false;
660
}
661
662
+ if(ri->cloud_provider_type && strcmp(ri->cloud_provider_type, "unknown") != 0)
663
+ strncpyz(ds->cloud_provider_type, ri->cloud_provider_type, sizeof(ds->cloud_provider_type) - 1);
664
+
665
+ if(ri->cloud_instance_type && strcmp(ri->cloud_instance_type, "unknown") != 0)
666
+ strncpyz(ds->cloud_instance_type, ri->cloud_instance_type, sizeof(ds->cloud_instance_type) - 1);
667
+
668
+ if(ri->cloud_instance_region && strcmp(ri->cloud_instance_region, "unknown") != 0)
669
+ strncpyz(ds->cloud_instance_region, ri->cloud_instance_region, sizeof(ds->cloud_instance_region) - 1);
670
+
671
ds->read_system_info = true;
672
}
src/libnetdata/exit/exit_initiated.c
+1
@@ -31,6 +31,7 @@ ENUM_STR_MAP_DEFINE(EXIT_REASON) = {
31
{ EXIT_REASON_SYSTEM_SHUTDOWN, "system-shutdown"},
32
33
{ EXIT_REASON_UPDATE, "update"},
34
+ { EXIT_REASON_SHUTDOWN_TIMEOUT, "shutdown-timeout"},
35
36
// terminator
37
{0, NULL},
src/libnetdata/exit/exit_initiated.h
+4
@@ -42,6 +42,9 @@ typedef enum {
42
43
// netdata update
44
EXIT_REASON_UPDATE = (1 << 18),
45
+
46
+ // timeout while shutting down
47
+ EXIT_REASON_SHUTDOWN_TIMEOUT = (1 << 19),
48
} EXIT_REASON;
49
50
#define EXIT_REASON_NORMAL \
@@ -74,6 +77,7 @@ typedef enum {
77
| EXIT_REASON_FATAL \
78
| EXIT_REASON_ALREADY_RUNNING \
79
| EXIT_REASON_OUT_OF_MEMORY \
80
+ | EXIT_REASON_SHUTDOWN_TIMEOUT \
81
)
82
83
#define is_deadly_signal(reason) ((reason) & (EXIT_REASON_DEADLY_SIGNAL))