1 /*******************************************************************************
2 3 Classes to write statistics to be used in graphite.
4 5 Applications that want to log statistics usually make use of the `StatsExt`
6 extension (most likely by deriving from `DaemonApp`),
7 which provides a `StatsLog` instance which is automatically configured from
8 the application's config.ini.
9 10 StatsLog provides methods to:
11 1. Build up a stats line by writing sets of values (specified by the
12 fields of one or more user-specified structs).
13 2. Flush the stats line to the output.
14 15 Currently, `StatsLog` writes to a file (called `stats.log`), which is then
16 parsed by a script that will feed the data to a Collectd socket.
17 Every server's Collectd daemon will then report to a master Collectd server
18 which aggregates the data.
19 As our number of stats is growing and the write rate is increasing, we're
20 planning to expose a way to directly write to the Collectd socket.
21 As a result, the current API of `StatsLog` is intentionally designed
22 to comply to the limitations of Collectd. See the documentation of
23 `StatsLog` for more details
24 25 Refer to the class' description for information about their actual usage.
26 27 Copyright:
28 Copyright (c) 2009-2016 dunnhumby Germany GmbH.
29 All rights reserved.
30 31 License:
32 Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
33 Alternatively, this file may be distributed under the terms of the Tango
34 3-Clause BSD License (see LICENSE_BSD.txt for details).
35 36 *******************************************************************************/37 38 moduleocean.util.log.Stats;
39 40 41 42 importocean.meta.types.Qualifiers;
43 importocean.core.Verify;
44 importocean.core.Verify;
45 importocean.core.Enforce;
46 importocean.core.ExceptionDefinitions;
47 importocean.meta.codegen.Identifier/* : fieldIdentifier */;
48 importocean.core.TypeConvert;
49 importocean.io.select.EpollSelectDispatcher;
50 importocean.io.select.client.TimerEvent;
51 importocean.net.collectd.Collectd;
52 importcore.stdc.time : time_t;
53 importocean.sys.ErrnoException;
54 importocean.text.convert.Formatter;
55 importocean.util.container.AppendBuffer;
56 importocean.util.log.layout.LayoutStatsLog;
57 importocean.util.log.Appender;
58 importocean.util.log.Logger;
59 60 61 version (unittest)
62 {
63 importocean.util.app.DaemonApp;
64 }
65 66 /*******************************************************************************
67 68 Transmit the values of an aggregate to be used within graphite.
69 70 This class has 2 methods which can be used: `add` and `addObject`.
71 72 `add` is meant for application statistics, i.e. amount of memory used,
73 number of channels alive, number of connections open, largest record
74 processed...
75 76 `addObject` logs an instance of an object which belongs to a category.
77 This method should be used when you have a set of standard metrics which
78 you want to log for multiple instances of a type of object.
79 For example, you may want to log standard stats for each channel in
80 a storage engine, for each campaign of an advertiser,
81 for each source of input records, etc.
82 83 See the methods description for more informations.
84 85 Note:
86 StatsLog formerly had the ability to write single value instead of an
87 aggregate. It was removed as it goes against Collectd's design, where
88 data sent to the socket are sent aggregated by 'types', where a type is
89 a collection of related metrics (akin to a `struct`), so single values
90 are not permitted.
91 In addition, it's not possible to incrementally build an aggregate either,
92 as we need the aggregate's complete definition : if we send incomplete/too
93 much data to Collectd, it just rejects the whole aggregate, and data is sent
94 without field names, as Collectd relies on its type definition for that
95 piece of information. Having the wrong order would mean some metrics are
96 logged as other metrics, a bug that might not be easily identifiable.
97 This was leaving too much room for error which were not easily identifiable.
98 99 Examples:
100 See the unittest following this class for an example application
101 102 *******************************************************************************/103 104 publicclassStatsLog105 {
106 importocean.util.log.AppendFile;
107 108 /***************************************************************************
109 110 Stats log config class
111 112 The field `hostname`, `app_name`, `app_instance` and `default_type`
113 are values used by the Collectd integration of `StatsLog`.
114 115 Collectd identify resources using an identifier, that has the
116 following form: 'hostname/plugin-pinstance/type-tinstance'.
117 Every resource written by a process MUST have the same 'hostname',
118 'plugin' and 'pinstance' values.
119 `hostname` value is not limited or checked in any way.
120 Other identifier shall only include alphanum ([a-z] [A-Z] [0-9]),
121 underscores ('_') and dots ('.').
122 Instance parts can also include dashes ('-').
123 124 ***************************************************************************/125 126 publicstaticclassConfig127 {
128 publicistringfile_name;
129 130 131 /***********************************************************************
132 133 Path to the collectd socket
134 135 It is null by default. When set through the constructor (usual value
136 is provided through `default_collectd_socket`, `StatsLog` will
137 write to the Collectd socket.
138 139 When this is set, it is required that `app_name` and
140 `app_instance` be set.
141 142 ***********************************************************************/143 144 publicistringsocket_path;
145 146 147 /***********************************************************************
148 149 'hostname' to use when logging using 'add'
150 151 This is the 'hostname' part of the identifier.
152 If not set, gethostname (2) will be called.
153 See this class' documentation for further details.
154 155 ***********************************************************************/156 157 publicistringhostname;
158 159 160 /***********************************************************************
161 162 Collectd 'plugin' name to used
163 164 This is the 'plugin' part of the identifier, and should be set
165 to your application's name. It should hardly ever change.
166 167 By default, the name provided to the application framework will be
168 used. If the application framework isn't used, or the name needs
169 to be overridden, set this value to a non-empty string.
170 171 See this class' documentation for further details.
172 173 ***********************************************************************/174 175 publicistringapp_name;
176 177 178 /***********************************************************************
179 180 Collectd 'plugin instance' name to used
181 182 This is the 'pinstance' part of the identifier, and should be set
183 to your application's "instance". This can be an id (1, 2, 3...)
184 or a more complicated string, like the ranges over which your app
185 operate ("0x00000000_0x0FFFFFFF"). Change to this value should be
186 rare, if any.
187 The duo of 'plugin' and 'pinstance' should uniquely identify
188 a process (for the same `hostname`).
189 190 See this class' documentation for further details.
191 192 ***********************************************************************/193 194 publicistringapp_instance;
195 196 197 /***********************************************************************
198 199 Default 'type' to use when logging using 'add'
200 201 This is the 'type' part of the identifier. Usually it is provided
202 as a string template argument to `addObject`, but for convenience,
203 `add` provide a default logging channel. If this argument is not
204 supplied, `collectd_name ~ "_stats"` will be used.
205 206 See this class' documentation for further details.
207 208 ***********************************************************************/209 210 publicistringdefault_type;
211 212 213 /***********************************************************************
214 215 Frequency at which stats are collected
216 217 This metric is expressed in seconds, and should rarely needs to be
218 modified. Defaults to 30s.
219 220 ***********************************************************************/221 222 publiculonginterval;
223 224 225 /***********************************************************************
226 227 Constructor
228 229 Emulates struct's default constructor by providing a default value
230 for all parameters.
231 232 ***********************************************************************/233 234 publicthis ( istringfile_name = default_file_name,
235 istringsocket_path = null,
236 istringhostname = null,
237 istringapp_name = null,
238 istringapp_instance = null,
239 istringdefault_type = null,
240 ulonginterval = 30)
241 242 {
243 this.file_name = file_name;
244 245 // Collectd settings246 this.socket_path = socket_path;
247 this.hostname = hostname;
248 this.app_name = app_name;
249 this.app_instance = app_instance;
250 this.default_type = default_type;
251 this.interval = interval;
252 }
253 }
254 255 256 /***************************************************************************
257 258 Stats log default settings (used in ctor)
259 260 ***************************************************************************/261 262 publicstaticimmutabletime_tdefault_period = 30; // 30 seconds263 publicstaticimmutableistringdefault_file_name = "log/stats.log";
264 265 266 /***************************************************************************
267 268 Logger instance via which error messages can be emitted
269 270 ***************************************************************************/271 272 privateLoggererror_log;
273 274 275 /***************************************************************************
276 277 Logger instance via which stats should be output
278 279 ***************************************************************************/280 281 protectedLoggerlogger;
282 283 284 /***************************************************************************
285 286 Message formatter
287 288 ***************************************************************************/289 290 protectedAppendBuffer!(char) buffer;
291 292 293 /***************************************************************************
294 295 Whether to add a separator or not
296 297 ***************************************************************************/298 299 privatebooladd_separator = false;
300 301 302 /***************************************************************************
303 304 Constructor. Creates the stats log using the AppendSysLog appender.
305 306 Params:
307 config = instance of the config class
308 name = name of the logger, should be set to a different string
309 when using more than two StatLogs
310 311 ***************************************************************************/312 313 publicthis ( Configconfig, istringname = "Stats" )
314 {
315 AppendernewAppender ( istringfile, Appender.Layoutlayout )
316 {
317 returnnewAppendFile(file, layout);
318 }
319 320 this(config, &newAppender, name);
321 }
322 323 324 /***************************************************************************
325 326 Constructor. Creates the stats log using the appender returned by the
327 provided delegate.
328 329 Params:
330 config = instance of the config class
331 new_appender = delegate which returns appender to use for stats log
332 name = name of the logger, should be set to a different string
333 when using more than two StatLogs
334 335 ***************************************************************************/336 337 publicthis ( Configconfig,
338 scopeAppenderdelegate ( istringfile, Appender.Layoutlayout ) new_appender,
339 istringname = "Stats" )
340 {
341 if (config.socket_path.length)
342 {
343 verify(config.hostname.length != 0);
344 verify(config.app_name.length != 0);
345 verify(config.default_type.length != 0);
346 }
347 348 // logger via which error messages can be emitted349 this.error_log = Log.lookup("ocean.util.log.Stats.StatsLog");
350 351 // logger to which stats should be written352 this.logger = Log.lookup(name);
353 this.logger.clear();
354 this.logger.additive(false);
355 356 this.logger.add(new_appender(config.file_name, newLayoutStatsLog));
357 358 // Explicitly set the logger to output all levels, to avoid the situation359 // where the root logger is configured to not output level 'info'.360 this.logger.level = this.logger.Level.Trace;
361 362 this.buffer = newAppendBuffer!(char);
363 364 if (config.socket_path.length)
365 {
366 // Will throw if it can't connect to the socket367 this.collectd = newCollectd(config.socket_path);
368 369 this.identifier.host = config.hostname;
370 this.identifier.plugin = config.app_name;
371 this.identifier.plugin_instance = config.app_instance;
372 this.identifier.type = config.default_type;
373 this.options.interval = config.interval;
374 }
375 }
376 377 378 /***************************************************************************
379 380 Adds the values of the given aggregate to the stats log. Each member
381 of the aggregate will be output as <member name>:<member value>.
382 383 Params:
384 values = aggregate containing values to write to the log.
385 386 ***************************************************************************/387 388 publictypeof(this) add ( T ) ( Tvalues )
389 {
390 staticassert (is(T == struct) || is(T == class),
391 "Parameter to add must be a struct or a class");
392 this.format!(null)(values, istring.init);
393 if (this.collectd !isnull)
394 this.sendToCollectd!(null)(values, istring.init);
395 returnthis;
396 }
397 398 399 /***************************************************************************
400 401 Adds the values of the given aggregate to the stats log. Each member of
402 the aggregate will be output as
403 <category>/<instance>/<member name>:<member value>.
404 405 Params:
406 category = The name of the category this object belongs to.
407 408 instance = Name of the object to add.
409 values = aggregate containing values to write to the log.
410 411 ***************************************************************************/412 413 publictypeof(this) addObject (istringcategory, T)
414 (cstringinstance, Tvalues)
415 {
416 staticassert (is(T == struct) || is(T == class),
417 "Parameter to add must be a struct or a class");
418 staticassert(category.length,
419 "Template parameter 'category' should not be null");
420 verify (instance.length != 0, "Object name should not be null");
421 422 this.format!(category)(values, instance);
423 if (this.collectd !isnull)
424 this.sendToCollectd!(category)(values, instance);
425 returnthis;
426 }
427 428 429 /***************************************************************************
430 431 Flush everything to file and prepare for the next iteration
432 433 ***************************************************************************/434 435 publicvoidflush ( )
436 {
437 this.logger.info(this.buffer[]);
438 this.add_separator = false;
439 this.buffer.clear();
440 }
441 442 443 /***************************************************************************
444 445 Writes the values from the provided aggregate to the format_buffer
446 member.
447 448 Each member of the aggregate is output as either:
449 <category name>/<object name>/<member name>:<member value>
450 if a category is provided, or as:
451 <member name>:<member value>
452 if no category is provided.
453 It's a runtime error to provide a category but no instance name, or the
454 other way around.
455 456 Note: When the aggregate is a class, the members of the super class
457 are not iterated over.
458 459 Params:
460 category = the type or category of the object, such as 'channels',
461 'users'... May be null (see the 'instance' parameter).
462 T = the type of the aggregate containing the fields to log
463 464 values = aggregate containing values to write to the log. Passed as
465 ref purely to avoid making a copy -- the aggregate is not
466 modified.
467 instance = the name of the instance of the category, or null if
468 none. For example, if the category is 'companies', then the name
469 of an instance may be "google". This value should be null if
470 category is null, and non-null otherwise.
471 472 ***************************************************************************/473 474 privatevoidformat ( istringcategory, T ) ( refTvalues, cstringinstance )
475 {
476 scopesink = (cstringv) { this.buffer ~= v; };
477 foreach ( i, value; values.tupleof )
478 {
479 autovalue_name = .identifier!(T.tupleof[i]);
480 481 staticif (is(typeof(value) : long))
482 longfmtd_value = value;
483 elsestaticif (is(typeof(value) : double))
484 doublefmtd_value = value;
485 else486 {
487 pragma(msg, "[", __FILE__, ":", __LINE__, "] '", T.stringof,
488 "' should only contain integer or floating point members");
489 autofmtd_value = value;
490 }
491 492 // stringof results in something like "values.somename", we want493 // only "somename"494 if (this.add_separator)
495 {
496 this.buffer ~= ' ';
497 }
498 499 staticif (category.length)
500 {
501 verify(instance.length != 0);
502 sformat(sink, "{}/{}/{}:{}",
503 category, instance, value_name, fmtd_value);
504 }
505 else506 {
507 verify(!instance.length);
508 sformat(sink, "{}:{}", value_name, fmtd_value);
509 }
510 511 this.add_separator = true;
512 }
513 }
514 515 516 /***************************************************************************
517 518 Send the content of the struct to Collectd
519 520 This will format and send data to the Collectd daemon.
521 If any error happen, they will be reported by a log message but won't
522 be propagated up, so applications that have trouble logging won't
523 fail.
524 525 Note:
526 As with `format`, when the aggregate is a class, the members of
527 the super class are not iterated over.
528 529 Params:
530 category = the type or category of the object, such as 'channels',
531 'users'... May be null (see the 'instance' parameter).
532 T = the type of the aggregate containing the fields to log
533 534 values = aggregate containing values to write to the log. Passed as
535 ref purely to avoid making a copy -- the aggregate is not
536 modified.
537 instance = the name of the instance of the category, or null if
538 none. For example, if the category is 'companies', then the name
539 of an instance may be "google". This value should be null if
540 category is null, and non-null otherwise.
541 542 ***************************************************************************/543 544 privatevoidsendToCollectd (istringcategory, T) (refTvalues,
545 cstringinstance)
546 {
547 verify(this.collectd !isnull);
548 staticif (!category.length)
549 verify(!instance.length);
550 551 // It's a value type (struct), do a copy552 autoid = this.identifier;
553 staticif (category.length)
554 {
555 id.type = category;
556 id.type_instance = instance;
557 }
558 559 // putval returns null on success, a failure reason else560 try561 this.collectd.putval!(T)(id, values, this.options);
562 catch (CollectdExceptione)
563 this.error_log.error("Sending stats to Collectd failed: {}", e);
564 catch (ErrnoExceptione)
565 this.error_log.error("I/O error while sending stats: {}", e);
566 }
567 568 569 /***************************************************************************
570 571 Collectd instance
572 573 ***************************************************************************/574 575 protectedCollectdcollectd;
576 577 578 /***************************************************************************
579 580 Default identifier when doing `add`.
581 582 ***************************************************************************/583 584 protectedIdentifieridentifier;
585 586 587 /***************************************************************************
588 589 Default set options to send
590 591 Currently it's only `interval=30`.
592 593 ***************************************************************************/594 595 protectedCollectd.PutvalOptionsoptions;
596 }
597 598 /// Usage example for StatsLog in a simple application599 unittest600 {
601 classMyStatsLogApp : DaemonApp602 {
603 privatestaticstructStats604 {
605 doubleawesomeness;
606 doublebytes_written;
607 doublebytes_received;
608 }
609 610 privatestaticstructChannel611 {
612 doubleprofiles_in;
613 doubleprofiles_out;
614 }
615 616 publicthis ()
617 {
618 super("Test", null, null);
619 }
620 621 protectedoverrideintrun (Argumentsargs, ConfigParserconfig)
622 {
623 autoepoll = newEpollSelectDispatcher;
624 this.startEventHandling(epoll);
625 return0;
626 }
627 628 protectedoverridevoidonStatsTimer ( )
629 {
630 // Do some heavy-duty processing ...631 Statsapp_stats1 = { 42_000_000, 10_000_000, 1_000_000 };
632 Statsapp_stats2 = { 42_000_000, 1_000_000, 10_000_000 };
633 this.stats_ext.stats_log.add(app_stats1);
634 635 // A given struct should be `add`ed once and only once, unless636 // you flush in between637 this.stats_ext.stats_log.flush();
638 this.stats_ext.stats_log.add(app_stats2);
639 640 // Though if you use `addObject`, it's okay as long as the instance641 // name is different642 Channeldisney = { 100_000, 100_000 };
643 Channeldiscovery = { 10_000, 10_000 };
644 645 // For the same struct type, you probably want the646 // same category name. It's not a requirement but there are647 // no known use case where you want it to differ.648 this.stats_ext.stats_log.addObject!("channel")("disney", disney);
649 this.stats_ext.stats_log.addObject!("channel")("discovery", discovery);
650 }
651 }
652 }