1 /******************************************************************************* 2 3 An identifier as defined by collectd 4 5 An Identifier is of the form 'host/plugin-instance/type-instance'. 6 Both '-instance' parts are optional. 7 'plugin' and each '-instance' part may be chosen freely as long as 8 the tuple (plugin, plugin instance, type instance) uniquely identifies 9 the plugin within collectd. 10 'type' identifies the type and number of values (i. e. data-set) 11 passed to collectd. 12 A large list of predefined data-sets is available in the types.db file. 13 14 See_Also: 15 https://collectd.org/documentation/manpages/collectd-unixsock.5.shtml 16 https://collectd.org/documentation/manpages/types.db.5.shtml 17 18 Copyright: 19 Copyright (c) 2015-2016 dunnhumby Germany GmbH. 20 All rights reserved. 21 22 License: 23 Boost Software License Version 1.0. See LICENSE_BOOST.txt for details. 24 Alternatively, this file may be distributed under the terms of the Tango 25 3-Clause BSD License (see LICENSE_BSD.txt for details). 26 27 *******************************************************************************/ 28 29 module ocean.net.collectd.Identifier; 30 31 32 33 import ocean.meta.types.Qualifiers; 34 import ocean.core.ExceptionDefinitions; // IllegalArgumentException 35 import ocean.text.convert.Formatter; 36 import ocean.text.util.StringSearch; // locateChar 37 38 version (unittest) 39 { 40 import ocean.core.Test; 41 } 42 43 /// Convenience alias 44 private alias StringSearch!(false).locateChar locateChar; 45 46 47 48 /******************************************************************************/ 49 50 public struct Identifier 51 { 52 /*************************************************************************** 53 54 Hostname 55 56 If null, inferred from a call to hostname. This call will only take 57 place once, at application startup, so it won't have any 58 performance impact on running applications. 59 It is recommended to use null. 60 61 ***************************************************************************/ 62 63 public cstring host; 64 65 66 /*************************************************************************** 67 68 Should be set to the application name 69 70 ***************************************************************************/ 71 72 public cstring plugin; 73 74 75 /*************************************************************************** 76 77 Application defined type 78 79 Types are defined in the file 'types.db'. 80 Each type correspond to a set of values, with a particular meaning. 81 For example, the type that would be used to log repeated invocation 82 of the 'df' command line utility would be a data type consisting of 83 two fields, 'used' and 'free'. 84 85 ***************************************************************************/ 86 87 public cstring type; 88 89 90 /*************************************************************************** 91 92 Application instance 93 94 It is recommended to use fixed numbers, such as [ 1, 2, 3, 4 ] if 95 you have, for example 4 instances of the application running 96 on the same 'host'. 97 You can also use names for an instance, however, the identifier should 98 be unique for this 'host', and should not change for the lifetime of 99 the application (so using PID as instance is discouraged for example); 100 101 ***************************************************************************/ 102 103 public cstring plugin_instance; 104 105 106 /*********************************************************************** 107 108 Application defined instance of the category ('type') 109 110 Each 'type' uniquely identify a kind of data, while 'type_instance' 111 link this to an instance of the data. For example, to continue 112 with the 'df' example (see 'type' documentation), 'type_instance' 113 would probably be set to mount points, such as 'dev', 'run', 'boot'. 114 115 ***********************************************************************/ 116 117 public cstring type_instance; 118 119 120 /*********************************************************************** 121 122 Sanity checks 123 124 ***********************************************************************/ 125 126 invariant () 127 { 128 assert(this.host.length, "No host for identifier"); 129 assert(this.plugin.length, "No plugin for identifier"); 130 assert(this.type.length, "No type for identifier"); 131 } 132 133 134 /*************************************************************************** 135 136 Convenience wrapper around `Identifier.create(cstring, Identifier)` 137 that throws on parsing error. 138 139 Params: 140 line = An string matching 'host/plugin-instance/type-instance'. 141 Both `instance` part are optionals, in which case the '-' 142 should be omitted. 143 144 Throws: 145 If the argument `line` is not a valid identifier. 146 147 Returns: 148 The parsed identifier. 149 150 ***************************************************************************/ 151 152 public static Identifier create (cstring line) 153 { 154 Identifier ret = void; // 'out' params are default initialized 155 if (auto msg = Identifier.create(line, ret)) 156 { 157 // Because the ctor doesn't expose it... 158 auto e = new IllegalArgumentException(format("{}: {}", line, msg)); 159 e.file = __FILE__; 160 e.line = __LINE__; 161 throw e; 162 } 163 return ret; 164 } 165 166 unittest 167 { 168 testThrown!(IllegalArgumentException)(Identifier.create("")); 169 testThrown!(IllegalArgumentException)(Identifier.create("/")); 170 testThrown!(IllegalArgumentException)(Identifier.create("//")); 171 testThrown!(IllegalArgumentException)(Identifier.create("a/b/-")); 172 testThrown!(IllegalArgumentException)(Identifier.create("a/-/c")); 173 testThrown!(IllegalArgumentException)(Identifier.create("a/b-/c")); 174 testThrown!(IllegalArgumentException)(Identifier.create("a/b-/c-")); 175 testThrown!(IllegalArgumentException)(Identifier.create("a/b/c/d")); 176 177 Identifier expected = { host: "a", plugin: "b", type: "c" }; 178 testStructEquality(Identifier.create("a/b/c"), expected); 179 180 expected.plugin_instance = "foo"; 181 testStructEquality(Identifier.create("a/b-foo/c"), expected); 182 183 expected.type_instance = "bar"; 184 testStructEquality(Identifier.create("a/b-foo/c-bar"), expected); 185 186 expected.type = "com.sociomantic.bytes_sent"; 187 testStructEquality( 188 Identifier.create("a/b-foo/com.sociomantic.bytes_sent-bar"), 189 expected); 190 } 191 192 193 /*************************************************************************** 194 195 Parse a string and returns the corresponding `Identifier` 196 197 If the string passed is not a valid identifier, this function will 198 return the reason why, else it returns `null`. 199 200 This function is useful to get an identifier out of Collectd, or 201 from any Collectd-formatted identifier. 202 To construct an identifier with known values, initializing the fields 203 is enough. 204 205 Params: 206 line = An string matching 'host/plugin-instance/type-instance'. 207 Both `instance` part are optionals, in which case the '-' 208 should be omitted. 209 identifier = An identifier to fill with the parsed string. 210 If this function returns non-null, the state of 211 `identifier` should not be relied upon. 212 213 Returns: 214 `null` if the parsing succeeded, else a string representing the 215 error. 216 217 ***************************************************************************/ 218 219 public static istring create (cstring line, out Identifier identifier) 220 { 221 if (!line.length) 222 return "Empty string is not a valid identifier"; 223 224 // Parses the host 225 { 226 auto slash = locateChar(line, '/'); 227 identifier.host = line[0 .. slash]; 228 line = line[slash + 1 .. $]; 229 } 230 231 // Parses the plugin and the optional instance 232 { 233 auto slash = locateChar(line, '/'); 234 if (slash >= line.length) 235 return "No plugin name found"; 236 auto dash = locateChar(line[0 .. slash], '-'); 237 if (dash < slash) 238 { 239 identifier.plugin = line[0 .. dash]; 240 identifier.plugin_instance = line[dash + 1 .. slash]; 241 if (auto m = check!("plugin instance")(identifier.plugin_instance)) 242 return m; 243 } 244 else 245 { 246 identifier.plugin = line[0 .. slash]; 247 identifier.plugin_instance = null; 248 } 249 line = line[slash + 1 .. $]; 250 } 251 252 // Finally, parses the type and the optional instance 253 { 254 auto dash = locateChar(line, '-'); 255 if (!line.length) 256 return "Empty type found"; 257 if (dash < line.length) 258 { 259 identifier.type = line[0 .. dash]; 260 identifier.type_instance = line[dash + 1 .. $]; 261 if (auto m = check!("type instance")(identifier.type_instance)) 262 return m; 263 } 264 else 265 { 266 identifier.type = line; 267 identifier.type_instance = null; 268 } 269 } 270 271 // We don't check host for validity, only that it contains something 272 if (!identifier.host.length) 273 return "Empty host found"; 274 if (auto msg = check!("plugin")(identifier.plugin)) 275 return msg; 276 if (auto msg = check!("type")(identifier.type)) 277 return msg; 278 279 return null; 280 } 281 282 283 version (unittest) 284 { 285 /*********************************************************************** 286 287 Replacement for `test!("==")(identifierA, identifierB)`, as it would 288 trigger an assertion failure when formatting the arguments. 289 290 ***********************************************************************/ 291 292 private static void testStructEquality (Identifier actual, 293 Identifier expected) 294 { 295 test!("==")(actual.host, expected.host); 296 test!("==")(actual.plugin, expected.plugin); 297 test!("==")(actual.type, expected.type); 298 test!("==")(actual.plugin_instance, expected.plugin_instance); 299 test!("==")(actual.type_instance, expected.type_instance); 300 } 301 } 302 303 /*************************************************************************** 304 305 Helper function template for validating a field's content 306 307 ***************************************************************************/ 308 309 private static istring check (istring fieldname) (cstring field) 310 { 311 if (!field.length) 312 return "Empty " ~ fieldname ~ " found"; 313 314 auto idx = invalidIndex(field); 315 if (field.length != idx) 316 return "Invalid char found in " ~ fieldname 317 ~ ", allowed chars are: [0-9][a-z][A-Z][._-]"; 318 return null; 319 } 320 321 322 /*************************************************************************** 323 324 Find the first forbidden char in an identifier, if any 325 326 Valid identifier are solely composed of [a-z][A-Z][0-9][._-] 327 If any char isn't in that list, this function returns its index. 328 329 Params: 330 str = String to validate 331 332 Returns: 333 The index at which the invalid char is, or `str.length` if there 334 isn't any 335 336 ***************************************************************************/ 337 338 public static size_t invalidIndex (cstring str) 339 { 340 foreach (idx, c; str) 341 { 342 if (!(c >= 'a' && c <= 'z') 343 && !(c >= 'A' && c <= 'Z') 344 && !(c >= '0' && c <= '9') 345 && !(c == '.' || c == '_' || c == '-')) 346 return idx; 347 } 348 return str.length; 349 } 350 351 352 /*************************************************************************** 353 354 This function is useful for debug, however it is not intended to be 355 used in production code, as it allocates. 356 357 Returns: 358 A newly-allocated string suitable for printing. 359 360 ***************************************************************************/ 361 362 public istring toString () 363 { 364 auto pi = this.plugin_instance.length ? "-" : null; 365 auto ti = this.type_instance.length ? "-" : null; 366 367 return format("{}/{}{}{}/{}{}{}", this.host, 368 this.plugin, pi, this.plugin_instance, 369 this.type, ti, this.type_instance); 370 } 371 }