1 #include "first.h"
2
3 #include "base.h"
4 #include "fdevent.h"
5 #include "log.h"
6 #include "response.h"
7
8 #include "plugin.h"
9 #include <sys/types.h>
10 #include <sys/stat.h>
11 #include "sys-time.h"
12
13 #include <fcntl.h>
14 #include <stdlib.h>
15 #include <string.h>
16 #include <unistd.h>
17 #include <errno.h>
18
19 typedef struct {
20 const buffer *path_rrd;
21 off_t requests;
22 off_t bytes_written;
23 off_t bytes_read;
24 } rrd_config;
25
26 typedef struct {
27 rrd_config *rrd;
28 } plugin_config;
29
30 typedef struct {
31 PLUGIN_DATA;
32 plugin_config defaults;
33 plugin_config conf;
34
35 int read_fd;
36 int write_fd;
37 pid_t rrdtool_pid;
38 pid_t srv_pid;
39
40 int rrdtool_running;
41 const buffer *path_rrdtool_bin;
42 server *srv;
43 } plugin_data;
44
INIT_FUNC(mod_rrd_init)45 INIT_FUNC(mod_rrd_init) {
46 return ck_calloc(1, sizeof(plugin_data));
47 }
48
mod_rrd_free_config(plugin_data * const p)49 static void mod_rrd_free_config(plugin_data * const p) {
50 if (NULL == p->cvlist) return;
51 /* (init i to 0 if global context; to 1 to skip empty global context) */
52 for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) {
53 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
54 for (; -1 != cpv->k_id; ++cpv) {
55 switch (cpv->k_id) {
56 case 0: /* rrdtool.db-name */
57 if (cpv->vtype == T_CONFIG_LOCAL) free(cpv->v.v);
58 break;
59 default:
60 break;
61 }
62 }
63 }
64 }
65
FREE_FUNC(mod_rrd_free)66 FREE_FUNC(mod_rrd_free) {
67 plugin_data *p = p_d;
68 if (NULL == p->srv) return;
69 mod_rrd_free_config(p);
70
71 if (p->read_fd >= 0) close(p->read_fd);
72 if (p->write_fd >= 0) close(p->write_fd);
73 if (p->rrdtool_pid > 0 && p->srv_pid == p->srv->pid) {
74 /* collect status (blocking) */
75 fdevent_waitpid(p->rrdtool_pid, NULL, 0);
76 }
77 }
78
mod_rrd_create_pipe(server * srv,plugin_data * p)79 static int mod_rrd_create_pipe(server *srv, plugin_data *p) {
80 char *args[3];
81 int to_rrdtool_fds[2];
82 int from_rrdtool_fds[2];
83 /* mod_rrdtool does not work with server.max-workers > 0
84 * since the data between workers is not aggregated,
85 * and it is not valid to send data to rrdtool more than once a sec
86 * (which would happen with multiple workers writing to same pipe)
87 * If pipes were to be shared, then existing pipes would need to be
88 * reused here, if they already exist (not -1), and after flushing
89 * existing contents (read and discard from read-end of pipes). */
90 if (fdevent_pipe_cloexec(to_rrdtool_fds, 4096)) {
91 log_perror(srv->errh, __FILE__, __LINE__, "pipe()");
92 return 0;
93 }
94 if (fdevent_pipe_cloexec(from_rrdtool_fds, 4096)) {
95 log_perror(srv->errh, __FILE__, __LINE__, "pipe()");
96 close(to_rrdtool_fds[0]);
97 close(to_rrdtool_fds[1]);
98 return 0;
99 }
100 const char * const path_rrdtool_bin = p->path_rrdtool_bin
101 ? p->path_rrdtool_bin->ptr
102 : "/usr/bin/rrdtool";
103 *(const char **)&args[0] = path_rrdtool_bin;
104 *(const char **)&args[1] = "-";
105 args[2] = NULL;
106
107 p->rrdtool_pid = fdevent_fork_execve(args[0], args, NULL, to_rrdtool_fds[0], from_rrdtool_fds[1], -1, -1);
108
109 if (-1 != p->rrdtool_pid) {
110 close(from_rrdtool_fds[1]);
111 close(to_rrdtool_fds[0]);
112 if (p->read_fd >= 0) close(p->read_fd);
113 if (p->write_fd >= 0) close(p->write_fd);
114 p->write_fd = to_rrdtool_fds[1];
115 p->read_fd = from_rrdtool_fds[0];
116 p->srv_pid = srv->pid;
117 return 1;
118 } else {
119 log_perror(srv->errh, __FILE__, __LINE__,
120 "fork/exec(%s)", path_rrdtool_bin);
121 close(to_rrdtool_fds[0]);
122 close(to_rrdtool_fds[1]);
123 close(from_rrdtool_fds[0]);
124 close(from_rrdtool_fds[1]);
125 return 0;
126 }
127 }
128
129 __attribute_noinline__
mod_rrd_exec(server * srv,plugin_data * p)130 static int mod_rrd_exec(server *srv, plugin_data *p) {
131 return (p->rrdtool_running = mod_rrd_create_pipe(srv, p));
132 }
133
mod_rrd_merge_config_cpv(plugin_config * const pconf,const config_plugin_value_t * const cpv)134 static void mod_rrd_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
135 switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */
136 case 0: /* rrdtool.db-name */
137 if (cpv->vtype == T_CONFIG_LOCAL) pconf->rrd = cpv->v.v;
138 break;
139 case 1: /* rrdtool.binary */ /* T_CONFIG_SCOPE_SERVER */
140 break;
141 default:/* should not happen */
142 return;
143 }
144 }
145
mod_rrd_merge_config(plugin_config * const pconf,const config_plugin_value_t * cpv)146 static void mod_rrd_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) {
147 do {
148 mod_rrd_merge_config_cpv(pconf, cpv);
149 } while ((++cpv)->k_id != -1);
150 }
151
mod_rrd_patch_config(request_st * const r,plugin_data * const p)152 static void mod_rrd_patch_config(request_st * const r, plugin_data * const p) {
153 p->conf = p->defaults; /* copy small struct instead of memcpy() */
154 /*memcpy(&p->conf, &p->defaults, sizeof(plugin_config));*/
155 for (int i = 1, used = p->nconfig; i < used; ++i) {
156 if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id))
157 mod_rrd_merge_config(&p->conf, p->cvlist + p->cvlist[i].v.u2[0]);
158 }
159 }
160
SETDEFAULTS_FUNC(mod_rrd_set_defaults)161 SETDEFAULTS_FUNC(mod_rrd_set_defaults) {
162 static const config_plugin_keys_t cpk[] = {
163 { CONST_STR_LEN("rrdtool.db-name"),
164 T_CONFIG_STRING,
165 T_CONFIG_SCOPE_CONNECTION }
166 ,{ CONST_STR_LEN("rrdtool.binary"),
167 T_CONFIG_STRING,
168 T_CONFIG_SCOPE_SERVER }
169 ,{ NULL, 0,
170 T_CONFIG_UNSET,
171 T_CONFIG_SCOPE_UNSET }
172 };
173
174 plugin_data * const p = p_d;
175 p->srv = srv;
176 if (!config_plugin_values_init(srv, p, cpk, "mod_rrdtool"))
177 return HANDLER_ERROR;
178
179 int activate = 0;
180
181 /* process and validate config directives
182 * (init i to 0 if global context; to 1 to skip empty global context) */
183 for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
184 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
185 for (; -1 != cpv->k_id; ++cpv) {
186 switch (cpv->k_id) {
187 case 0: /* rrdtool.db-name */
188 if (!buffer_is_blank(cpv->v.b)) {
189 rrd_config *rrd = ck_calloc(1, sizeof(rrd_config));
190 rrd->path_rrd = cpv->v.b;
191 cpv->v.v = rrd;
192 cpv->vtype = T_CONFIG_LOCAL;
193 activate = 1;
194 }
195 break;
196 case 1: /* rrdtool.binary */ /* T_CONFIG_SCOPE_SERVER */
197 if (!buffer_is_blank(cpv->v.b))
198 p->path_rrdtool_bin = cpv->v.b; /*(store directly in p)*/
199 break;
200 default:/* should not happen */
201 break;
202 }
203 }
204 }
205
206 /* initialize p->defaults from global config context */
207 if (p->nconfig > 0 && p->cvlist->v.u2[1]) {
208 const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0];
209 if (-1 != cpv->k_id)
210 mod_rrd_merge_config(&p->defaults, cpv);
211 }
212
213 p->rrdtool_running = 0;
214 p->read_fd = -1;
215 p->write_fd = -1;
216
217 return (!activate || mod_rrd_exec(srv, p)) ? HANDLER_GO_ON : HANDLER_ERROR;
218 }
219
220 /* read/write wrappers to catch EINTR */
221
222 /* write to blocking socket; blocks until all data is sent, write returns 0 or an error (apart from EINTR) occurs. */
safe_write(int fd,const void * buf,size_t count)223 static ssize_t safe_write(int fd, const void *buf, size_t count) {
224 ssize_t res, sum = 0;
225
226 for (;;) {
227 res = write(fd, buf, count);
228 if (res >= 0) {
229 sum += res;
230 /* do not try again if res == 0 */
231 if (res == 0 || (size_t) res == count) return sum;
232 count -= res;
233 buf = (const char*) buf + res;
234 continue;
235 }
236 switch (errno) {
237 case EINTR:
238 continue;
239 default:
240 return -1;
241 }
242 }
243 }
244
245 /* this assumes we get enough data on a successful read */
safe_read(int fd,char * buf,size_t sz)246 static ssize_t safe_read(int fd, char *buf, size_t sz) {
247 ssize_t res;
248
249 do {
250 res = read(fd, buf, sz-1);
251 } while (-1 == res && errno == EINTR);
252
253 if (res >= 0) buf[res] = '\0';
254 return res;
255 }
256
mod_rrdtool_create_rrd(server * srv,plugin_data * p,rrd_config * s,char * resp,size_t respsz)257 static int mod_rrdtool_create_rrd(server *srv, plugin_data *p, rrd_config *s, char *resp, size_t respsz) {
258 struct stat st;
259
260 /* check if DB already exists */
261 if (0 == stat(s->path_rrd->ptr, &st)) {
262 /* check if it is plain file */
263 if (!S_ISREG(st.st_mode)) {
264 log_error(srv->errh, __FILE__, __LINE__,
265 "not a regular file: %s", s->path_rrd->ptr);
266 return HANDLER_ERROR;
267 }
268
269 /* still create DB if it's empty file */
270 if (st.st_size > 0) {
271 return HANDLER_GO_ON;
272 }
273 }
274
275 /* create a new one */
276 buffer * const cmd = srv->tmp_buf;
277 buffer_clear(cmd);
278 buffer_append_str3(cmd,
279 CONST_STR_LEN("create "),
280 BUF_PTR_LEN(s->path_rrd),
281 CONST_STR_LEN(
282 " --step 60 "
283 "DS:InOctets:ABSOLUTE:600:U:U "
284 "DS:OutOctets:ABSOLUTE:600:U:U "
285 "DS:Requests:ABSOLUTE:600:U:U "
286 "RRA:AVERAGE:0.5:1:600 "
287 "RRA:AVERAGE:0.5:6:700 "
288 "RRA:AVERAGE:0.5:24:775 "
289 "RRA:AVERAGE:0.5:288:797 "
290 "RRA:MAX:0.5:1:600 "
291 "RRA:MAX:0.5:6:700 "
292 "RRA:MAX:0.5:24:775 "
293 "RRA:MAX:0.5:288:797 "
294 "RRA:MIN:0.5:1:600 "
295 "RRA:MIN:0.5:6:700 "
296 "RRA:MIN:0.5:24:775 "
297 "RRA:MIN:0.5:288:797\n"));
298
299 if (-1 == (safe_write(p->write_fd, BUF_PTR_LEN(cmd)))) {
300 log_perror(srv->errh, __FILE__, __LINE__, "rrdtool-write: failed");
301 return HANDLER_ERROR;
302 }
303
304 if (-1 == safe_read(p->read_fd, resp, respsz)) {
305 log_perror(srv->errh, __FILE__, __LINE__, "rrdtool-read: failed");
306 return HANDLER_ERROR;
307 }
308
309 if (resp[0] != 'O' || resp[1] != 'K') {
310 log_error(srv->errh, __FILE__, __LINE__,
311 "rrdtool-response: %s %s", cmd->ptr, resp);
312 return HANDLER_ERROR;
313 }
314
315 return HANDLER_GO_ON;
316 }
317
318 __attribute_cold__
mod_rrd_fatal_error(plugin_data * p)319 static int mod_rrd_fatal_error(plugin_data *p) {
320 /* future: might send kill() signal to p->rrdtool_pid to trigger restart */
321 p->rrdtool_running = 0;
322 return 0;
323 }
324
325 __attribute_noinline__
mod_rrd_write_data(server * srv,plugin_data * p,rrd_config * s)326 static int mod_rrd_write_data(server *srv, plugin_data *p, rrd_config *s) {
327 char resp[4096];
328
329 if (HANDLER_GO_ON != mod_rrdtool_create_rrd(srv, p, s, resp, sizeof(resp)))
330 return 0;
331
332 buffer * const cmd = srv->tmp_buf;
333 buffer_clear(cmd);
334 buffer_append_str3(cmd, CONST_STR_LEN("update "),
335 BUF_PTR_LEN(s->path_rrd),
336 CONST_STR_LEN(" N:"));
337 buffer_append_int(cmd, s->bytes_read);
338 buffer_append_char(cmd, ':');
339 buffer_append_int(cmd, s->bytes_written);
340 buffer_append_char(cmd, ':');
341 buffer_append_int(cmd, s->requests);
342 buffer_append_char(cmd, '\n');
343
344 if (-1 == safe_write(p->write_fd, BUF_PTR_LEN(cmd))) {
345 log_error(srv->errh, __FILE__, __LINE__, "rrdtool-write: failed");
346 return mod_rrd_fatal_error(p);
347 }
348
349 if (-1 == safe_read(p->read_fd, resp, sizeof(resp))) {
350 log_error(srv->errh, __FILE__, __LINE__, "rrdtool-read: failed");
351 return mod_rrd_fatal_error(p);
352 }
353
354 if (resp[0] == 'O' && resp[1] == 'K') {
355 s->requests = 0;
356 s->bytes_written = 0;
357 s->bytes_read = 0;
358 }
359 else if (!(strstr(resp, "(minimum one second step)")
360 && log_epoch_secs - srv->startup_ts < 3)) {
361 /* don't fail on this error if we just started (above condition)
362 * (graceful restart, the old one might have just updated too) */
363 log_error(srv->errh, __FILE__, __LINE__,
364 "rrdtool-response: %s %s", cmd->ptr, resp);
365 return mod_rrd_fatal_error(p);
366 }
367
368 return 1;
369 }
370
mod_rrd_write_data_loop(server * srv,plugin_data * p)371 static void mod_rrd_write_data_loop(server *srv, plugin_data *p) {
372 /* process and validate config directives
373 * (init i to 0 if global context; to 1 to skip empty global context) */
374 for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
375 config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
376 for (; -1 != cpv->k_id; ++cpv) {
377 switch (cpv->k_id) {
378 case 0: /* rrdtool.db-name */
379 if (cpv->vtype != T_CONFIG_LOCAL) continue;
380 mod_rrd_write_data(srv, p, cpv->v.v);
381 if (!p->rrdtool_running) return;
382 break;
383 }
384 }
385 }
386 }
387
TRIGGER_FUNC(mod_rrd_trigger)388 TRIGGER_FUNC(mod_rrd_trigger) {
389 plugin_data *p = p_d;
390 /*(0 == p->rrdtool_pid if never activated; not used)*/
391 if (0 == p->rrdtool_pid) return HANDLER_GO_ON;
392
393 /* write data once a minute */
394 if ((log_epoch_secs % 60) != 0) return HANDLER_GO_ON;
395
396 if (!p->rrdtool_running) {
397 if (srv->pid != p->srv_pid) return HANDLER_GO_ON;
398 /* restart limited to once every 60 sec (above) */
399 if (!mod_rrd_exec(srv, p)) return HANDLER_GO_ON;
400 }
401
402 mod_rrd_write_data_loop(srv, p);
403 return HANDLER_GO_ON;
404 }
405
mod_rrd_waitpid_cb(server * srv,void * p_d,pid_t pid,int status)406 static handler_t mod_rrd_waitpid_cb(server *srv, void *p_d, pid_t pid, int status) {
407 plugin_data *p = p_d;
408 if (pid != p->rrdtool_pid) return HANDLER_GO_ON;
409 if (srv->pid != p->srv_pid) return HANDLER_GO_ON;
410
411 p->rrdtool_running = 0;
412 p->rrdtool_pid = -1;
413
414 UNUSED(status);
415 return HANDLER_FINISHED;
416 }
417
REQUESTDONE_FUNC(mod_rrd_account)418 REQUESTDONE_FUNC(mod_rrd_account) {
419 plugin_data *p = p_d;
420 /*(0 == p->rrdtool_pid if never activated; not used)*/
421 if (0 == p->rrdtool_pid) return HANDLER_GO_ON;
422
423 mod_rrd_patch_config(r, p);
424 rrd_config * const rrd = p->conf.rrd;
425 if (NULL != rrd) {
426 ++rrd->requests;
427 rrd->bytes_written += http_request_stats_bytes_out(r);
428 rrd->bytes_read += http_request_stats_bytes_in(r);
429 }
430 return HANDLER_GO_ON;
431 }
432
433
434 __attribute_cold__
435 int mod_rrdtool_plugin_init(plugin *p);
mod_rrdtool_plugin_init(plugin * p)436 int mod_rrdtool_plugin_init(plugin *p) {
437 p->version = LIGHTTPD_VERSION_ID;
438 p->name = "rrd";
439
440 p->init = mod_rrd_init;
441 p->cleanup = mod_rrd_free;
442 p->set_defaults= mod_rrd_set_defaults;
443
444 p->handle_trigger = mod_rrd_trigger;
445 p->handle_waitpid = mod_rrd_waitpid_cb;
446 p->handle_request_done = mod_rrd_account;
447
448 return 0;
449 }
450