├── .editorconfig ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── catapult.conf ├── daemon.conf ├── files └── test_config.conf ├── install.sh └── src ├── config.rs ├── filters.rs ├── inputs ├── mod.rs ├── network.rs ├── random.rs └── stdin.rs ├── main.rs ├── outputs ├── elasticsearch.rs ├── file.rs ├── mod.rs ├── network.rs └── stdout.rs └── processor.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [*.rs] 10 | charset = utf-8 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.o 3 | *.so 4 | *.rlib 5 | *.dll 6 | 7 | # Executables 8 | *.exe 9 | 10 | # Generated by Cargo 11 | /target/ 12 | 13 | logs 14 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "catapult" 3 | version = "0.1.2" 4 | dependencies = [ 5 | "chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)", 6 | "docopt 0.6.86 (registry+https://github.com/rust-lang/crates.io-index)", 7 | "hyper 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", 8 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 9 | "nom 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)", 10 | "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 11 | "serde 0.6.15 (registry+https://github.com/rust-lang/crates.io-index)", 12 | "serde_json 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", 13 | "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", 14 | "url 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)", 15 | ] 16 | 17 | [[package]] 18 | name = "aho-corasick" 19 | version = "0.5.3" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | dependencies = [ 22 | "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", 23 | ] 24 | 25 | [[package]] 26 | name = "bitflags" 27 | version = "0.3.3" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | 30 | [[package]] 31 | name = "chrono" 32 | version = "0.2.25" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | dependencies = [ 35 | "num 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", 36 | "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", 37 | ] 38 | 39 | [[package]] 40 | name = "cookie" 41 | version = "0.1.21" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | dependencies = [ 44 | "openssl 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", 45 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 46 | "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", 47 | "url 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)", 48 | ] 49 | 50 | [[package]] 51 | name = "docopt" 52 | version = "0.6.86" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | dependencies = [ 55 | "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 56 | "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)", 57 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 58 | "strsim 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 59 | ] 60 | 61 | [[package]] 62 | name = "dtoa" 63 | version = "0.2.2" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | 66 | [[package]] 67 | name = "gcc" 68 | version = "0.3.38" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | 71 | [[package]] 72 | name = "hpack" 73 | version = "0.2.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | dependencies = [ 76 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 77 | ] 78 | 79 | [[package]] 80 | name = "httparse" 81 | version = "1.1.2" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | 84 | [[package]] 85 | name = "hyper" 86 | version = "0.6.16" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | dependencies = [ 89 | "cookie 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", 90 | "httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 91 | "language-tags 0.0.7 (registry+https://github.com/rust-lang/crates.io-index)", 92 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 93 | "mime 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 94 | "num_cpus 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", 95 | "openssl 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", 96 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 97 | "solicit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", 98 | "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", 99 | "traitobject 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 100 | "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 101 | "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 102 | "url 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)", 103 | ] 104 | 105 | [[package]] 106 | name = "itoa" 107 | version = "0.1.1" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | 110 | [[package]] 111 | name = "kernel32-sys" 112 | version = "0.2.2" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | dependencies = [ 115 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 116 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 117 | ] 118 | 119 | [[package]] 120 | name = "language-tags" 121 | version = "0.0.7" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | 124 | [[package]] 125 | name = "lazy_static" 126 | version = "0.1.16" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | 129 | [[package]] 130 | name = "lazy_static" 131 | version = "0.2.1" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | 134 | [[package]] 135 | name = "libc" 136 | version = "0.1.12" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | 139 | [[package]] 140 | name = "libc" 141 | version = "0.2.17" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | 144 | [[package]] 145 | name = "libressl-pnacl-sys" 146 | version = "2.1.6" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | dependencies = [ 149 | "pnacl-build-helper 1.4.10 (registry+https://github.com/rust-lang/crates.io-index)", 150 | ] 151 | 152 | [[package]] 153 | name = "log" 154 | version = "0.3.6" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | 157 | [[package]] 158 | name = "matches" 159 | version = "0.1.3" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | 162 | [[package]] 163 | name = "memchr" 164 | version = "0.1.11" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | dependencies = [ 167 | "libc 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)", 168 | ] 169 | 170 | [[package]] 171 | name = "mime" 172 | version = "0.1.3" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | dependencies = [ 175 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 176 | "serde 0.6.15 (registry+https://github.com/rust-lang/crates.io-index)", 177 | ] 178 | 179 | [[package]] 180 | name = "nom" 181 | version = "1.2.4" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | 184 | [[package]] 185 | name = "num" 186 | version = "0.1.36" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | dependencies = [ 189 | "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 190 | "num-iter 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 191 | "num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", 192 | ] 193 | 194 | [[package]] 195 | name = "num-integer" 196 | version = "0.1.32" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | dependencies = [ 199 | "num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", 200 | ] 201 | 202 | [[package]] 203 | name = "num-iter" 204 | version = "0.1.32" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | dependencies = [ 207 | "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 208 | "num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", 209 | ] 210 | 211 | [[package]] 212 | name = "num-traits" 213 | version = "0.1.36" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | 216 | [[package]] 217 | name = "num_cpus" 218 | version = "0.2.13" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | dependencies = [ 221 | "libc 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)", 222 | ] 223 | 224 | [[package]] 225 | name = "openssl" 226 | version = "0.6.7" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | dependencies = [ 229 | "bitflags 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 230 | "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", 231 | "libc 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", 232 | "openssl-sys 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", 233 | ] 234 | 235 | [[package]] 236 | name = "openssl-sys" 237 | version = "0.6.7" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | dependencies = [ 240 | "gcc 0.3.38 (registry+https://github.com/rust-lang/crates.io-index)", 241 | "libc 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", 242 | "libressl-pnacl-sys 2.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 243 | "pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", 244 | ] 245 | 246 | [[package]] 247 | name = "pkg-config" 248 | version = "0.3.8" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | 251 | [[package]] 252 | name = "pnacl-build-helper" 253 | version = "1.4.10" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | dependencies = [ 256 | "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 257 | ] 258 | 259 | [[package]] 260 | name = "rand" 261 | version = "0.3.14" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | dependencies = [ 264 | "libc 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)", 265 | ] 266 | 267 | [[package]] 268 | name = "regex" 269 | version = "0.1.80" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | dependencies = [ 272 | "aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", 273 | "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", 274 | "regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", 275 | "thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 276 | "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 277 | ] 278 | 279 | [[package]] 280 | name = "regex-syntax" 281 | version = "0.3.9" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | 284 | [[package]] 285 | name = "rustc-serialize" 286 | version = "0.3.19" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | 289 | [[package]] 290 | name = "rustc_version" 291 | version = "0.1.7" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | dependencies = [ 294 | "semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)", 295 | ] 296 | 297 | [[package]] 298 | name = "semver" 299 | version = "0.1.20" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | 302 | [[package]] 303 | name = "serde" 304 | version = "0.6.15" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | dependencies = [ 307 | "num 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", 308 | ] 309 | 310 | [[package]] 311 | name = "serde" 312 | version = "0.8.17" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | 315 | [[package]] 316 | name = "serde_json" 317 | version = "0.8.3" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | dependencies = [ 320 | "dtoa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 321 | "itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 322 | "num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", 323 | "serde 0.8.17 (registry+https://github.com/rust-lang/crates.io-index)", 324 | ] 325 | 326 | [[package]] 327 | name = "solicit" 328 | version = "0.4.4" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | dependencies = [ 331 | "hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 332 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 333 | ] 334 | 335 | [[package]] 336 | name = "strsim" 337 | version = "0.5.1" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | 340 | [[package]] 341 | name = "tempdir" 342 | version = "0.3.5" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | dependencies = [ 345 | "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 346 | ] 347 | 348 | [[package]] 349 | name = "thread-id" 350 | version = "2.0.0" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | dependencies = [ 353 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 354 | "libc 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)", 355 | ] 356 | 357 | [[package]] 358 | name = "thread_local" 359 | version = "0.2.7" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | dependencies = [ 362 | "thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 363 | ] 364 | 365 | [[package]] 366 | name = "time" 367 | version = "0.1.35" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | dependencies = [ 370 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 371 | "libc 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)", 372 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 373 | ] 374 | 375 | [[package]] 376 | name = "traitobject" 377 | version = "0.0.1" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | 380 | [[package]] 381 | name = "typeable" 382 | version = "0.1.2" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | 385 | [[package]] 386 | name = "unicase" 387 | version = "1.4.0" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | dependencies = [ 390 | "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 391 | ] 392 | 393 | [[package]] 394 | name = "url" 395 | version = "0.2.38" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | dependencies = [ 398 | "matches 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 399 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 400 | "uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", 401 | ] 402 | 403 | [[package]] 404 | name = "utf8-ranges" 405 | version = "0.1.3" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | 408 | [[package]] 409 | name = "uuid" 410 | version = "0.1.18" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | dependencies = [ 413 | "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 414 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 415 | ] 416 | 417 | [[package]] 418 | name = "winapi" 419 | version = "0.2.8" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | 422 | [[package]] 423 | name = "winapi-build" 424 | version = "0.1.1" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | 427 | [metadata] 428 | "checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66" 429 | "checksum bitflags 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "32866f4d103c4e438b1db1158aa1b1a80ee078e5d77a59a2f906fd62a577389c" 430 | "checksum chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00" 431 | "checksum cookie 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "02443c47d5c80f9b4be9b8f51c0bf307d663fe28b18ccabef44d8b0a4b2a967b" 432 | "checksum docopt 0.6.86 (registry+https://github.com/rust-lang/crates.io-index)" = "4a7ef30445607f6fc8720f0a0a2c7442284b629cf0d049286860fae23e71c4d9" 433 | "checksum dtoa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0dd841b58510c9618291ffa448da2e4e0f699d984d436122372f446dae62263d" 434 | "checksum gcc 0.3.38 (registry+https://github.com/rust-lang/crates.io-index)" = "553f11439bdefe755bf366b264820f1da70f3aaf3924e594b886beb9c831bcf5" 435 | "checksum hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2da7d3a34cf6406d9d700111b8eafafe9a251de41ae71d8052748259343b58" 436 | "checksum httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "46534074dbb80b070d60a5cb8ecadd8963a00a438ae1a95268850a7ef73b67ae" 437 | "checksum hyper 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)" = "99f86de19a680224688020785c0b62a2dccace29719847b16f1a9b22c312827f" 438 | "checksum itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae3088ea4baeceb0284ee9eea42f591226e6beaecf65373e41b38d95a1b8e7a1" 439 | "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 440 | "checksum language-tags 0.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "41633f9c0d99840437d1f2073c0a6dadcf1dbd28b87dda956e3d91b65f6e57a7" 441 | "checksum lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "cf186d1a8aa5f5bee5fd662bc9c1b949e0259e1bcc379d1f006847b0080c7417" 442 | "checksum lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "49247ec2a285bb3dcb23cbd9c35193c025e7251bfce77c1d5da97e6362dffe7f" 443 | "checksum libc 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "e32a70cf75e5846d53a673923498228bbec6a8624708a9ea5645f075d6276122" 444 | "checksum libc 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)" = "044d1360593a78f5c8e5e710beccdc24ab71d1f01bc19a29bcacdba22e8475d8" 445 | "checksum libressl-pnacl-sys 2.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "cbc058951ab6a3ef35ca16462d7642c4867e6403520811f28537a4e2f2db3e71" 446 | "checksum log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ab83497bf8bf4ed2a74259c1c802351fcd67a65baa86394b6ba73c36f4838054" 447 | "checksum matches 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bcc3ad8109fa4b522f9b0cd81440422781f564aaf8c195de6b9d6642177ad0dd" 448 | "checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" 449 | "checksum mime 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ec0c2f4d901bf1d4a2192a40b4b570ae3b19c51243e549defc1de741940aa787" 450 | "checksum nom 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" 451 | "checksum num 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "bde7c03b09e7c6a301ee81f6ddf66d7a28ec305699e3d3b056d2fc56470e3120" 452 | "checksum num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "fb24d9bfb3f222010df27995441ded1e954f8f69cd35021f6bef02ca9552fb92" 453 | "checksum num-iter 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "287a1c9969a847055e1122ec0ea7a5c5d6f72aad97934e131c83d5c08ab4e45c" 454 | "checksum num-traits 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "a16a42856a256b39c6d3484f097f6713e14feacd9bfb02290917904fae46c81c" 455 | "checksum num_cpus 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "cee7e88156f3f9e19bdd598f8d6c9db7bf4078f99f8381f43a55b09648d1a6e3" 456 | "checksum openssl 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "816776e562d5e95935ffb09a45442deb846c4767f1c3c3b33ec24a4b8106e11a" 457 | "checksum openssl-sys 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "74a0b047568fe3d4f35a076d6bd3c49b5ae7da80c4df72956b3a8268020f1aab" 458 | "checksum pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8cee804ecc7eaf201a4a207241472cc870e825206f6c031e3ee2a72fa425f2fa" 459 | "checksum pnacl-build-helper 1.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "61c9231d31aea845007443d62fcbb58bb6949ab9c18081ee1e09920e0cf1118b" 460 | "checksum rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2791d88c6defac799c3f20d74f094ca33b9332612d9aef9078519c82e4fe04a5" 461 | "checksum regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f" 462 | "checksum regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957" 463 | "checksum rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)" = "6159e4e6e559c81bd706afe9c8fd68f547d3e851ce12e76b1de7914bab61691b" 464 | "checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084" 465 | "checksum semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)" = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac" 466 | "checksum serde 0.6.15 (registry+https://github.com/rust-lang/crates.io-index)" = "c97b18e9e53de541f11e497357d6c5eaeb39f0cb9c8734e274abe4935f6991fa" 467 | "checksum serde 0.8.17 (registry+https://github.com/rust-lang/crates.io-index)" = "784e249221c84265caeb1e2fe48aeada86f67f5acb151bd3903c4585969e43f6" 468 | "checksum serde_json 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1cb6b19e74d9f65b9d03343730b643d729a446b29376785cd65efdff4675e2fc" 469 | "checksum solicit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "172382bac9424588d7840732b250faeeef88942e37b6e35317dce98cafdd75b2" 470 | "checksum strsim 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "50c069df92e4b01425a8bf3576d5d417943a6a7272fbabaf5bd80b1aaa76442e" 471 | "checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6" 472 | "checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03" 473 | "checksum thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5" 474 | "checksum time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "3c7ec6d62a20df54e07ab3b78b9a3932972f4b7981de295563686849eb3989af" 475 | "checksum traitobject 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "07eaeb7689bb7fca7ce15628319635758eda769fed481ecfe6686ddef2600616" 476 | "checksum typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" 477 | "checksum unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a5906ca2b98c799f4b1ab4557b76367ebd6ae5ef14930ec841c74aed5f3764" 478 | "checksum url 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "cbaa8377a162d88e7d15db0cf110c8523453edcbc5bc66d2b6fffccffa34a068" 479 | "checksum utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" 480 | "checksum uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "78c590b5bd79ed10aad8fb75f078a59d8db445af6c743e55c4a53227fc01c13f" 481 | "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 482 | "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 483 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Pierre Baillet "] 3 | description = "Catapult sends you logs elsewhere" 4 | documentation = "http://people.zoy.org/~oct/public/doc/catapult/inputs/index.html" 5 | homepage = "https://github.com/octplane/catapult" 6 | keywords = ["network", "logs", "shipping", "logstash"] 7 | name = "catapult" 8 | license = "MIT/Apache-2.0" 9 | readme = "README.md" 10 | repository = "https://github.com/octplane/catapult" 11 | version = "0.1.2" 12 | 13 | [dependencies] 14 | chrono = "0.2.16" 15 | docopt = "0.6.74" 16 | hyper = "0.6.15" 17 | log = "0.3.6" 18 | nom = "1.0.0" 19 | rand = "0.3.11" 20 | serde = "0.6.1" 21 | serde_json = "0.8.3" 22 | time = "0.1.33" 23 | url = "0.2.37" 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:wheezy 2 | MAINTAINER Johannes Schickling "schickling.j@gmail.com" 3 | 4 | # needed by cargo 5 | ENV USER root 6 | 7 | ADD install.sh install.sh 8 | RUN chmod +x /install.sh 9 | RUN /install.sh 10 | RUN rm /install.sh 11 | 12 | VOLUME ["/source"] 13 | WORKDIR /source 14 | CMD ["bash"] 15 | 16 | ADD . /source 17 | RUN cargo build --release && mkdir -p /usr/share/catapult/bin && cp ./target/release/catapult /usr/share/catapult/bin/ 18 | VOLUME ["/usr/share/catapult"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Pierre Baillet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | target/release/catapult: 2 | docker build -t octplane/catapult:latest . 3 | clean: 4 | rm -rf target 5 | doc: target/doc/catapult/index.html 6 | cargo doc 7 | upload_doc: doc 8 | rsync -az target/doc/ oct@zoy.org:~/public_html/public/doc/ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Catapult 2 | 3 | Catapult is a small replacement for logstash. It has been designed to read logs in various 4 | places and send them to other places. 5 | 6 | Catapult is used in production to fetch Docker logs from container and send them 7 | to a central location. 8 | 9 | # Usage 10 | 11 | Catapult runs with a configuration that describes how to use it. 12 | This configuration is passed to the binary via the `-c` option. 13 | 14 | Please refer to the [API documentation](http://people.zoy.org/~oct/public/doc/catapult/) 15 | 16 | # Installing 17 | 18 | ``` 19 | # on OSX only 20 | export OPENSSL_INCLUDE_DIR=/usr/local/opt/openssl/include 21 | cargo build --release 22 | ``` 23 | 24 | or 25 | 26 | ``` 27 | make 28 | ``` 29 | 30 | If you want to build the linux binary inside a container 31 | 32 | # Contributors 33 | 34 | - Łukasz Niemier / hauleth : Backport to support beta rust 35 | 36 | # Licence 37 | 38 | Licensed under either of 39 | 40 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 41 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 42 | 43 | at your option. 44 | 45 | ### Contribution 46 | 47 | Unless you explicitly state otherwise, any contribution intentionally submitted 48 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 49 | -------------------------------------------------------------------------------- /catapult.conf: -------------------------------------------------------------------------------- 1 | input { 2 | random { 3 | fieldlist = "id:u32,content:str" 4 | rate = 3 5 | } 6 | } 7 | 8 | output { 9 | stdout 10 | # 11 | # network { 12 | # destination = "localhost" 13 | # port = 12121 14 | # } 15 | } 16 | -------------------------------------------------------------------------------- /daemon.conf: -------------------------------------------------------------------------------- 1 | input { 2 | network { 3 | listenPort = 12121 4 | } 5 | } 6 | 7 | output { 8 | file { 9 | directory = "./logs/" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /files/test_config.conf: -------------------------------------------------------------------------------- 1 | input { 2 | # this will be ignored { 3 | file { # file input 4 | pipo = 12 5 | path = "some literal string" # file path of source file 6 | } 7 | # inline comments are ignored too. 8 | stdin { 9 | tag = stdin 10 | } 11 | } 12 | 13 | output { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # exit if a command fails 4 | set -e 5 | 6 | triple=x86_64-unknown-linux-gnu 7 | 8 | # install curl (needed to install rust) 9 | apt-get update && apt-get install -y curl gdb g++-multilib lib32stdc++6 libssl-dev libncurses5-dev 10 | 11 | # install rust 12 | curl -sL https://static.rust-lang.org/dist/rust-nightly-$triple.tar.gz | tar xvz -C /tmp 13 | /tmp/rust-nightly-$triple/install.sh 14 | 15 | # install cargo 16 | curl -sL https://static.rust-lang.org/cargo-dist/cargo-nightly-$triple.tar.gz | tar xvz -C /tmp 17 | /tmp/cargo-nightly-$triple/install.sh 18 | 19 | # cleanup package manager 20 | apt-get remove --purge -y curl && apt-get autoclean && apt-get clean 21 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 22 | 23 | # prepare dir 24 | mkdir /source 25 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | 3 | use nom::{IResult, multispace, eof, alphanumeric, space, not_line_ending}; 4 | use nom::IResult::*; 5 | 6 | use std::io::prelude::*; 7 | use std::fs::File; 8 | use std::collections::HashMap; 9 | 10 | named!(quoted_string <&str>, 11 | chain!( 12 | tag!("\"") ~ 13 | qs: map_res!( 14 | take_until!("\""), 15 | str::from_utf8) ~ 16 | tag!("\"") , 17 | || { qs } 18 | ) 19 | ); 20 | 21 | named!(object_symbol_name <&[u8], &str>, map_res!(alphanumeric, str::from_utf8)); 22 | 23 | named!(blanks, 24 | chain!( 25 | many0!(alt!(multispace | comment | eol | eof)), 26 | || { &b""[..] })); 27 | 28 | named!(comment, 29 | chain!( 30 | tag!("#") ~ 31 | not_line_ending? ~ 32 | alt!(eol | eof) , 33 | || { &b""[..] })); 34 | 35 | named!(eol, 36 | alt!(tag!("\r\n") | tag!("\n") | tag!("\u{2028}") | tag!("\u{2029}"))); 37 | 38 | 39 | named!(key_value <&[u8],(&str,&str)>, 40 | chain!( 41 | key: map_res!(alphanumeric, str::from_utf8) ~ 42 | space? ~ 43 | tag!("=") ~ 44 | space? ~ 45 | val: alt!( 46 | quoted_string | 47 | map_res!( 48 | take_until_either!("\n\r#"), 49 | str::from_utf8 50 | ) 51 | ) ~ 52 | blanks , 53 | ||{(key, val)} 54 | ) 55 | ); 56 | 57 | 58 | named!(keys_and_values_aggregator<&[u8], Vec<(&str,&str)> >, 59 | chain!( 60 | tag!("{") ~ 61 | blanks ~ 62 | kva: many0!(key_value) ~ 63 | blanks ~ 64 | tag!("}") , 65 | || {kva} ) 66 | ); 67 | 68 | fn keys_and_values(input:&[u8]) -> IResult<&[u8], HashMap > { 69 | let mut h: HashMap = HashMap::new(); 70 | 71 | match keys_and_values_aggregator(input) { 72 | IResult::Done(i, tuple_vec) => { 73 | for &(k,v) in tuple_vec.iter() { 74 | h.insert(k.to_owned(), v.to_owned()); 75 | } 76 | IResult::Done(i, h) 77 | }, 78 | IResult::Incomplete(a) => IResult::Incomplete(a), 79 | IResult::Error(a) => IResult::Error(a) 80 | } 81 | } 82 | 83 | 84 | named!(object_and_params <&[u8], (String, Option>)>, 85 | chain!( 86 | blanks ~ 87 | ik: object_symbol_name ~ 88 | blanks ~ 89 | kv: keys_and_values? ~ 90 | blanks , 91 | || { (ik.to_lowercase(), kv) } 92 | ) 93 | ); 94 | 95 | named!(inputs <&[u8], Vec<(String, Option>)> >, 96 | chain!( 97 | tag!("input") ~ 98 | blanks ~ 99 | tag!("{") ~ 100 | blanks ~ 101 | ins: many0!(object_and_params) ~ 102 | blanks ~ 103 | tag!("}") ~ 104 | blanks , 105 | || { (ins) } 106 | ) 107 | ); 108 | 109 | named!(outputs <&[u8], Vec<(String, Option>)> >, 110 | chain!( 111 | tag!("output") ~ 112 | blanks ~ 113 | tag!("{") ~ 114 | blanks ~ 115 | outs: many0!(object_and_params) ~ 116 | blanks ~ 117 | tag!("}") ~ 118 | blanks , 119 | || { (outs) } 120 | ) 121 | ); 122 | 123 | #[derive(Debug)] 124 | pub struct Configuration { 125 | pub inputs: Vec<(String, Option>)>, 126 | pub outputs: Vec<(String, Option>)>, 127 | filters: Vec<(String, Option>)>, 128 | } 129 | 130 | named!(configuration <&[u8], Configuration>, 131 | chain!( 132 | inputs: inputs ~ 133 | blanks ~ 134 | outputs: outputs , 135 | || { 136 | Configuration{ 137 | inputs: inputs, 138 | outputs: outputs, 139 | filters: Vec::new() 140 | } 141 | } 142 | ) 143 | ); 144 | 145 | 146 | 147 | pub fn read_config_file(filename: &str) -> Result { 148 | println!("Reading config file."); 149 | let mut f = File::open(filename).unwrap(); 150 | let mut s = String::new(); 151 | 152 | match f.read_to_string(&mut s) { 153 | Ok(_) => { 154 | let source = s.into_bytes(); 155 | match configuration(&source) { 156 | Done(_, configuration) => Ok(configuration), 157 | Error(e) => { 158 | Err(format!("Parse error: {:?}", e)) 159 | }, 160 | Incomplete(e) => { 161 | Err(format!("Incomplete content -> await: {:?}", e)) 162 | } 163 | } 164 | }, 165 | Err(e) => Err(format!("Read error: {:?}", e)) 166 | } 167 | } 168 | 169 | #[test] 170 | fn test_config_parser() { 171 | match read_config_file("files/test_config.conf") { 172 | Ok(conf) => { 173 | println!("{:?}", conf); 174 | // Some({"path": "some literal string", "pipo": "12"})), (Stdin, Some({"tag": "stdin"}))] 175 | assert_eq!(conf.inputs.len(), 2); 176 | assert_eq!(conf.inputs[0].0, "file"); 177 | let mut file_conf = HashMap::new(); 178 | file_conf.insert("path".to_owned(), "some literal string".to_owned()); 179 | file_conf.insert("pipo".to_owned(), "12".to_owned()); 180 | assert_eq!(conf.inputs[0].1, Some(file_conf) ); 181 | 182 | assert_eq!(conf.inputs[1].0, "stdin"); 183 | let mut stdin_conf = HashMap::new(); 184 | stdin_conf.insert("tag".to_owned(), "stdin".to_owned()); 185 | assert_eq!(conf.inputs[1].1, Some(stdin_conf) ); 186 | 187 | 188 | }, 189 | Err(e) => assert!(false, format!("Unable to parse configuration file: {}", e)) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/filters.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | 3 | use serde_json; 4 | use serde_json::value; 5 | use serde_json::Value; 6 | use chrono::offset::utc::UTC; 7 | 8 | #[allow(dead_code)] 9 | fn int_to_level(level: u64) -> String { 10 | match level { 11 | 10 => "trace".to_string(), 12 | 20 => "debug".to_string(), 13 | 30 => "info".to_string(), 14 | 40 => "warn".to_string(), 15 | 50 => "error".to_string(), 16 | 60 => "fatal".to_string(), 17 | _ => format!("Unknown level {}", level) 18 | } 19 | } 20 | 21 | #[allow(dead_code)] 22 | pub fn transform(input_value: &mut Value) -> Value { 23 | // {"name":"stakhanov","hostname":"Quark.local","pid":65470,"level":30 24 | // "msg":"pushing http://fr.wikipedia.org/wiki/Giant_Sand", 25 | // "time":"2015-05-21T10:11:02.132Z","v":0} 26 | // 27 | // entry['@timestamp'] = entry.time; 28 | // entry.level = levels[entry.level]; 29 | // entry.message = entry.msg; 30 | // delete entry.time; 31 | // delete entry.msg; 32 | let mut input = input_value.as_object_mut().unwrap(); 33 | 34 | if input.contains_key("time") { 35 | let time = input.get("time").unwrap().clone(); 36 | input.insert("@timestamp".to_string(), time); 37 | input.remove("time"); 38 | } else { 39 | // Inject now timestamp. 40 | let tm = UTC::now(); 41 | 42 | let format_prefix = "%Y-%m-%dT%H:%M:%S.%f"; 43 | let format_suffix = "%Z"; 44 | // truncate up to the third digit 45 | // 2015-05-21T15:27:20.994 46 | // 01234567890123456789012 47 | let mut timestamp = tm.format(format_prefix.as_ref()).to_string(); 48 | timestamp.truncate(23); 49 | let timestamp_suffix = tm.format(format_suffix.as_ref()).to_string(); 50 | timestamp.push_str(×tamp_suffix); 51 | 52 | input.insert("@timestamp".to_string(), value::to_value(×tamp)); 53 | } 54 | 55 | if input.contains_key("level") { 56 | let level = input.get("level").unwrap().as_u64().unwrap(); 57 | input.insert("level".to_string(), value::to_value(&int_to_level(level))); 58 | } 59 | 60 | if input.contains_key("msg") { 61 | let message = input.get("msg").unwrap().clone(); 62 | input.insert("message".to_string(), message); 63 | input.remove("msg"); 64 | } 65 | return value::to_value(input); 66 | } 67 | 68 | #[allow(dead_code)] 69 | pub fn time_to_index_name(full_timestamp: &str) -> String { 70 | // compatible with "2015-05-21T10:11:02.132Z" 71 | let mut input = full_timestamp.to_string(); 72 | input.truncate(10); 73 | input = input.replace("-", "."); 74 | format!("logstash-{}", input) 75 | } 76 | 77 | #[test] 78 | fn it_transform_ok() { 79 | // let src = r#"{"name":"stakhanov","hostname":"Quark.local","pid":65470,"level":30,"msg":"pushing http://fr.wikipedia.org/wiki/Giant_Sand","time":"2015-05-21T10:11:02.132Z","v":0}"#; 80 | let src = r#"{"level":30, "msg":"this is a test.", "time": "12"}"#; 81 | let mut decode = serde_json::from_str::(src).unwrap(); 82 | let transformed = transform(&mut decode); 83 | let out = serde_json::to_string(&transformed).unwrap(); 84 | assert_eq!(out, r#"{"@timestamp":"12","level":"info","message":"this is a test."}"#); 85 | } 86 | 87 | #[test] 88 | fn it_prepares_index_name() { 89 | // let src = r#"{"name":"stakhanov","hostname":"Quark.local","pid":65470,"level":30,"msg":"pushing http://fr.wikipedia.org/wiki/Giant_Sand","time":"2015-05-21T10:11:02.132Z","v":0}"#; 90 | let src = r#"{"time": "2015-05-21T10:11:02.132Z"}"#; 91 | let decode = serde_json::from_str::(src).unwrap(); 92 | match decode.find("time") { 93 | Some(time) => assert_eq!("logstash-2015.05.21", time_to_index_name(time.as_string().unwrap())), 94 | None => assert!(false) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/inputs/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod stdin; 2 | pub mod random; 3 | pub mod network; 4 | -------------------------------------------------------------------------------- /src/inputs/network.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::mpsc::{Receiver, SyncSender}; 3 | use std::net::UdpSocket; 4 | 5 | use processor::{InputProcessor, ConfigurableFilter}; 6 | /// # Network input 7 | /// 8 | /// - listens on an UDP port for data, forward upstream 9 | /// 10 | /// ### catapult.conf 11 | /// 12 | /// ``` 13 | /// input { 14 | /// network { 15 | /// listenPort = 6667 16 | /// } 17 | /// } 18 | /// ``` 19 | /// ### Parameters 20 | /// 21 | /// - **listenPort**: UDP port to listen to 22 | 23 | 24 | pub struct Network { 25 | name: String 26 | } 27 | 28 | impl Network { 29 | pub fn new(name: String) -> Network { 30 | Network{ name: name } 31 | } 32 | } 33 | 34 | impl ConfigurableFilter for Network { 35 | fn human_name(&self) -> &str { 36 | self.name.as_ref() 37 | } 38 | 39 | fn mandatory_fields(&self) -> Vec<&str> { 40 | vec!["listenPort"] 41 | } 42 | 43 | } 44 | 45 | impl InputProcessor for Network { 46 | fn start(&self, config: &Option>) -> Receiver { 47 | self.requires_fields(config, self.mandatory_fields()); 48 | self.invoke(config, Network::handle_func) 49 | } 50 | fn handle_func(tx: SyncSender, oconfig: Option>) { 51 | let config = oconfig.expect("Need a configuration"); 52 | let listen_port = config.get("listenPort").expect("Need a listen port").parse::().unwrap(); 53 | 54 | let udp = UdpSocket::bind(&*format!("0.0.0.0:{}", listen_port)).unwrap(); 55 | 56 | loop { 57 | let mut buf = [0; 1024]; 58 | let _ = udp.recv_from(&mut buf).unwrap(); 59 | let l = String::from_utf8_lossy(&buf).into_owned(); 60 | let ll = l.clone(); 61 | match tx.try_send(l) { 62 | Ok(()) => {}, 63 | Err(e) => { 64 | println!("Unable to send line to processor: {}", e); 65 | println!("{}", ll) 66 | } 67 | } 68 | 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/inputs/random.rs: -------------------------------------------------------------------------------- 1 | extern crate rand as rnd; 2 | 3 | use std::collections::HashMap; 4 | use std::sync::mpsc::{Receiver, SyncSender}; 5 | use std::thread::sleep; 6 | use std::time::Duration; 7 | use self::rnd::{thread_rng, Rng}; 8 | 9 | use processor::{InputProcessor, ConfigurableFilter}; 10 | 11 | struct StringField; 12 | struct UInt32Field; 13 | 14 | trait Randomizable { 15 | fn generate(&self) -> String; 16 | } 17 | 18 | impl Randomizable for StringField { 19 | fn generate(&self) -> String { 20 | let s:String = thread_rng().gen_ascii_chars().take(10).collect(); 21 | s 22 | } 23 | } 24 | 25 | impl Randomizable for UInt32Field { 26 | fn generate(&self) -> String { 27 | format!("{}", thread_rng().gen::()) 28 | } 29 | } 30 | 31 | 32 | pub struct Random { 33 | name: String 34 | } 35 | 36 | /// # Random input 37 | /// 38 | /// - generate fake input according to column definitions 39 | /// 40 | /// ### catapult.conf 41 | /// 42 | /// ``` 43 | /// input { 44 | /// random { 45 | /// fieldlist = "id:id,content:str" 46 | /// rate = 3 47 | /// } 48 | /// } 49 | /// ``` 50 | /// ### Parameters 51 | /// 52 | /// - **rate**: Number of messages per second 53 | /// - **fieldList**: comma separated list of field name : type 54 | /// 55 | /// ### Supported type 56 | /// 57 | /// - String for now. All other types use string. 58 | 59 | 60 | impl Random { 61 | pub fn new(name: String) -> Random { 62 | Random{ name: name } 63 | } 64 | } 65 | 66 | fn typeize(f: &str) -> Box { 67 | let definition: Vec<&str> = f.split(":").collect(); 68 | match definition[1] { 69 | "u32" => Box::new(UInt32Field) as Box, 70 | _ => Box::new(StringField) as Box 71 | } 72 | } 73 | 74 | impl ConfigurableFilter for Random { 75 | fn human_name(&self) -> &str { 76 | self.name.as_ref() 77 | } 78 | fn mandatory_fields(&self) -> Vec<&str> { 79 | vec!["fieldlist", "rate"] 80 | } 81 | 82 | } 83 | 84 | impl InputProcessor for Random { 85 | fn start(&self, config: &Option>) -> Receiver { 86 | self.requires_fields(config, self.mandatory_fields()); 87 | self.invoke(config, Random::handle_func) 88 | } 89 | fn handle_func(tx: SyncSender, config: Option>) { 90 | let conf = config.unwrap(); 91 | let rate = conf.get("rate").unwrap().clone(); 92 | 93 | let sleep_duration: u32 = (1000.0f32 / rate.parse::().unwrap()) as u32; 94 | println!("Random input will sleep for {}", sleep_duration); 95 | 96 | let fields: Vec> = conf.get("fieldlist").unwrap().split(",").map(move |f| typeize(f)).collect(); 97 | 98 | loop { 99 | let duration = Duration::new(sleep_duration as u64, 0); 100 | sleep(duration); 101 | let mut l = Vec::new(); 102 | for f in &fields { 103 | l.push(f.generate()); 104 | } 105 | let line = l.join("\t"); 106 | match tx.try_send(line.clone()) { 107 | Ok(()) => {}, 108 | Err(e) => { 109 | println!("Unable to send line to processor: {}", e); 110 | println!("{}", line) 111 | } 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/inputs/stdin.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::io::prelude::*; 3 | use std::collections::HashMap; 4 | use std::sync::mpsc::{Receiver, SyncSender}; 5 | 6 | use processor::{InputProcessor, ConfigurableFilter}; 7 | 8 | /// # Stdin input 9 | /// 10 | /// - reads stdin 11 | /// 12 | /// ### catapult.conf 13 | /// 14 | /// ``` 15 | /// input { 16 | /// stdin 17 | /// } 18 | /// ``` 19 | /// ### Parameters 20 | /// 21 | /// - none 22 | 23 | pub struct Stdin { 24 | name: String 25 | } 26 | 27 | impl Stdin { 28 | pub fn new(name: String) -> Stdin { 29 | Stdin{ name: name } 30 | } 31 | } 32 | 33 | impl ConfigurableFilter for Stdin { 34 | fn human_name(&self) -> &str { 35 | self.name.as_ref() 36 | } 37 | } 38 | 39 | impl InputProcessor for Stdin { 40 | fn start(&self, config: &Option>) -> Receiver { 41 | self.invoke(config, Stdin::handle_func) 42 | } 43 | fn handle_func(tx: SyncSender, _config: Option>) { 44 | let stdin = io::stdin(); 45 | 46 | for line in stdin.lock().lines() { 47 | let l = line.unwrap(); 48 | let ll = l.clone(); 49 | match tx.try_send(l) { 50 | Ok(()) => {}, 51 | Err(e) => { 52 | println!("Unable to send line to processor: {}", e); 53 | println!("{}", ll) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Catapult is a simple replacement for logstash written in Rust 2 | //! 3 | //! It aims at being a simple logshipper that read logs from its inputs, transforms them using its filters 4 | //! and send them to its outputs. 5 | 6 | #[macro_use] 7 | extern crate nom; 8 | 9 | #[macro_use] 10 | extern crate log; 11 | 12 | extern crate chrono; 13 | extern crate hyper; 14 | extern crate serde; 15 | extern crate serde_json; 16 | extern crate time; 17 | extern crate url; 18 | 19 | extern crate docopt; 20 | 21 | pub mod config; 22 | pub mod inputs; 23 | pub mod outputs; 24 | pub mod filters; 25 | pub mod processor; 26 | 27 | use docopt::Docopt; 28 | use processor::{InputProcessor, OutputProcessor}; 29 | 30 | // Write the Docopt usage string. dfrites ? 31 | static USAGE: &'static str = " 32 | Usage: catapult [-c CONFIGFILE] 33 | catapult (--help | -h) 34 | 35 | Options: 36 | -h, --help Show this screen. 37 | -c CONFIGFILE Configuration file [default: catapult.conf] 38 | "; 39 | 40 | #[allow(dead_code)] 41 | fn main() { 42 | // Parse argv and exit the program with an error message if it fails. 43 | let args = Docopt::new(USAGE) 44 | .and_then(|d| d.argv(std::env::args().into_iter()).parse()) 45 | .unwrap_or_else(|e| e.exit()); 46 | 47 | let config_file = args.get_str("-c"); 48 | 49 | let configuration = config::read_config_file(config_file); 50 | match configuration { 51 | Ok(conf) => { 52 | let ref input = conf.inputs[0]; 53 | let ref datasource_name = input.0; 54 | let ref args = conf.inputs[0].1; 55 | let data_input = match datasource_name.as_ref() { 56 | "stdin" => inputs::stdin::Stdin::new(datasource_name.to_owned()).start(args), 57 | "random" => inputs::random::Random::new(datasource_name.to_owned()).start(args), 58 | "network" => inputs::network::Network::new(datasource_name.to_owned()).start(args), 59 | unsupported => { panic!("Input {} not implemented", unsupported)} 60 | }; 61 | 62 | let ref output = conf.outputs[0]; 63 | let ref dataoutput_name = output.0; 64 | let ref oargs = output.1; 65 | let data_output = match output.0.as_ref() { 66 | "stdout" => outputs::stdout::Stdout::new(dataoutput_name.to_owned()).start(data_input, oargs), 67 | "network" => outputs::network::Network::new(dataoutput_name.to_owned()).start(data_input, oargs), 68 | "file" => outputs::file::RotatingFile::new(dataoutput_name.to_owned()).start(data_input, oargs), 69 | "elasticsearch" => outputs::elasticsearch::Elasticsearch::new(dataoutput_name.to_owned()).start(data_input, oargs), 70 | unsupported => { panic!("Output {} not implemented", unsupported)} 71 | }; 72 | 73 | let _p = data_output.unwrap().join(); 74 | 75 | }, 76 | Err(e) => panic!("{:?}", e) 77 | }; 78 | 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/outputs/elasticsearch.rs: -------------------------------------------------------------------------------- 1 | use serde_json; 2 | use serde_json::value; 3 | use serde_json::Value; 4 | use serde_json::ser; 5 | 6 | use hyper::{ Client, Url}; 7 | use hyper::client::Body; 8 | 9 | use processor::{OutputProcessor, ConfigurableFilter}; 10 | 11 | use std; 12 | use std::collections::HashMap; 13 | use std::sync::mpsc::Receiver; 14 | use std::thread::JoinHandle; 15 | 16 | use std::net::UdpSocket; 17 | 18 | use filters::{transform, time_to_index_name}; 19 | 20 | pub struct Elasticsearch { 21 | name: String 22 | } 23 | 24 | impl Elasticsearch { 25 | pub fn new(name: String) -> Elasticsearch { 26 | Elasticsearch{ name: name } 27 | } 28 | } 29 | 30 | impl ConfigurableFilter for Elasticsearch { 31 | fn human_name(&self) -> &str { 32 | self.name.as_ref() 33 | } 34 | 35 | fn mandatory_fields(&self) -> Vec<&str> { 36 | vec!["destination", "port"] 37 | } 38 | } 39 | 40 | impl OutputProcessor for Elasticsearch { 41 | fn start(&self, rx:Receiver, config: &Option>) -> Result, String> { 42 | self.requires_fields(config, self.mandatory_fields()); 43 | self.invoke(rx, config, Elasticsearch::handle_func) 44 | } 45 | fn handle_func(rx: Receiver, oconfig: Option>) { 46 | let config = oconfig.expect("Need a configuration"); 47 | 48 | let destination_ip = config.get("destination").expect("Need a destination IP").clone(); 49 | let destination_port = config.get("port").expect("Need a destination port").parse::().unwrap(); 50 | 51 | loop { 52 | match rx.recv() { 53 | Ok(l) => { 54 | match serde_json::from_str::(l.as_ref()) { 55 | Ok(decoded) => { 56 | let mut mutable_decoded = decoded; 57 | let transformed = transform(&mut mutable_decoded); 58 | 59 | println!("{:?}", transformed); 60 | 61 | let index_name: Option = match transformed.find("@timestamp") { 62 | Some(time) => match time.as_str() { 63 | Some(t) => Some(time_to_index_name(t)), 64 | None => { 65 | error!("Failed to stringify."); 66 | 67 | None 68 | } 69 | }, 70 | None => { 71 | error!("Failed to find timestamp."); 72 | 73 | None 74 | } 75 | }; 76 | 77 | if !index_name.is_some() { 78 | continue; 79 | } 80 | 81 | let index_name = index_name.unwrap(); 82 | 83 | let typ = "logs"; 84 | let output = ser::to_string(&transformed).ok().unwrap(); 85 | let mut client = Client::new(); 86 | 87 | let url = format!("http://{}:{}/{}/{}?op_type=create", destination_ip, destination_port, index_name, typ ); 88 | 89 | let uri = Url::parse(&url).ok().expect("malformed url"); 90 | 91 | debug!("Posting to elasticsearch with url: {}", url); 92 | 93 | let body = output.into_bytes(); 94 | 95 | let res = client.post(uri) 96 | .body(Body::BufBody(&*body, body.len())) 97 | .send() 98 | .unwrap(); 99 | 100 | debug!("{:?}", res); 101 | }, 102 | Err(s) => println!("Unable to parse line: {}\nfor {}",s,l) 103 | } 104 | }, 105 | Err(std::sync::mpsc::RecvError) => break 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/outputs/file.rs: -------------------------------------------------------------------------------- 1 | use processor::{OutputProcessor, ConfigurableFilter}; 2 | 3 | use std::collections::HashMap; 4 | use std::sync::mpsc::Receiver; 5 | use std::thread::JoinHandle; 6 | use std::path::PathBuf; 7 | use std::io::prelude::*; 8 | use std::fs; 9 | use std::fs::File; 10 | use std::fs::create_dir_all; 11 | use std::fs::OpenOptions; 12 | 13 | 14 | use time; 15 | 16 | /// # File output 17 | /// 18 | /// - sends output into a rotating file 19 | /// 20 | /// ### catapult.conf 21 | /// 22 | /// ``` 23 | /// output { 24 | /// file { 25 | /// directory = "./logs/" 26 | /// } 27 | /// } 28 | /// ``` 29 | /// ### Parameters 30 | /// 31 | /// - **directory**: Base directory into which logs are created. Can be a strftime pattern. 32 | 33 | pub struct RotatingFile { 34 | name: String 35 | } 36 | 37 | impl RotatingFile { 38 | pub fn new(name: String) -> RotatingFile { 39 | RotatingFile{ name: name } 40 | } 41 | } 42 | 43 | impl ConfigurableFilter for RotatingFile { 44 | fn human_name(&self) -> &str { 45 | self.name.as_ref() 46 | } 47 | 48 | fn mandatory_fields(&self) -> Vec<&str> { 49 | vec!["directory"] 50 | } 51 | 52 | 53 | } 54 | 55 | impl OutputProcessor for RotatingFile { 56 | fn start(&self, rx: Receiver, config: &Option>) -> Result, String> { 57 | self.requires_fields(config, self.mandatory_fields()); 58 | self.invoke(rx, config, RotatingFile::handle_func) 59 | } 60 | fn handle_func(rx: Receiver, oconfig: Option>) { 61 | let config = oconfig.expect("Need a configuration"); 62 | let mut basefile_format = config.get("directory").expect("Need a log directory").clone(); 63 | 64 | basefile_format.push_str("%Y-%m-%d-%H:00.log"); 65 | 66 | let mut parent_dir = PathBuf::from("/"); 67 | let mut log_path = PathBuf::from(""); 68 | let mut log_file: Option = None; 69 | 70 | loop { 71 | 72 | match rx.recv() { 73 | Ok(mut l) => { 74 | let now = time::now(); 75 | let basefile = time::strftime(basefile_format.as_ref(), &now).ok().unwrap(); 76 | let new_log_path = PathBuf::from(basefile); 77 | let new_parent_dir = new_log_path.parent().unwrap().to_path_buf(); 78 | if new_parent_dir != parent_dir { 79 | match fs::metadata(new_parent_dir.as_path()) { 80 | Err(_) => {create_dir_all(new_parent_dir.as_path()).ok();}, 81 | _ => {} 82 | } 83 | parent_dir = new_parent_dir.clone(); 84 | } 85 | 86 | // First time we see this file, open it, or create it 87 | if new_log_path != log_path { 88 | log_path = new_log_path.clone(); 89 | match fs::metadata(log_path.as_path()) { 90 | Err(_) => { 91 | log_file = File::create(log_path.clone()).ok() 92 | }, 93 | Ok(f) => { 94 | if f.is_file() { 95 | log_file = OpenOptions::new().write(true).append(true).open(log_path.clone()).ok(); 96 | } else { 97 | panic!("File {:?} exists and is not a file.", log_path); 98 | } 99 | } 100 | } 101 | } 102 | l.push_str("\n"); 103 | if let Some(ref mut writable) = log_file { 104 | let _count = writable.write_all(l.as_bytes()); 105 | } else { 106 | println!("No file to write to, discarding line: {}", l); 107 | } 108 | } 109 | Err(e) => { panic!(e) } 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/outputs/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod stdout; 2 | pub mod elasticsearch; 3 | pub mod file; 4 | pub mod network; 5 | -------------------------------------------------------------------------------- /src/outputs/network.rs: -------------------------------------------------------------------------------- 1 | use processor::{OutputProcessor, ConfigurableFilter}; 2 | 3 | use std::collections::HashMap; 4 | use std::sync::mpsc::Receiver; 5 | use std::thread::JoinHandle; 6 | 7 | use std::net::UdpSocket; 8 | 9 | /// # Network output 10 | /// 11 | /// - sends output on the network using UDP 12 | /// 13 | /// ### catapult.conf 14 | /// 15 | /// ``` 16 | /// output { 17 | /// network { 18 | /// destination = "127.0.0.1" 19 | /// port = 12121 20 | /// } 21 | /// } 22 | /// ``` 23 | /// ### Parameters 24 | /// 25 | /// - **destination**: IP/name Destination 26 | /// - **port**: Destination Port 27 | 28 | 29 | pub struct Network { 30 | name: String 31 | } 32 | 33 | impl Network { 34 | pub fn new(name: String) -> Network { 35 | Network{ name: name } 36 | } 37 | } 38 | 39 | impl ConfigurableFilter for Network { 40 | fn human_name(&self) -> &str { 41 | self.name.as_ref() 42 | } 43 | 44 | fn mandatory_fields(&self) -> Vec<&str> { 45 | vec!["destination", "port"] 46 | } 47 | } 48 | 49 | impl OutputProcessor for Network { 50 | fn start(&self, rx:Receiver, config: &Option>) -> Result, String> { 51 | self.requires_fields(config, self.mandatory_fields()); 52 | self.invoke(rx, config, Network::handle_func) 53 | } 54 | fn handle_func(rx: Receiver, oconfig: Option>) { 55 | let config = oconfig.expect("Need a configuration"); 56 | 57 | let destination_ip = config.get("destination").expect("Need a destination IP").clone(); 58 | let destination_port = config.get("port").expect("Need a destination port").parse::().unwrap(); 59 | 60 | let udp = UdpSocket::bind("0.0.0.0:0").unwrap(); 61 | let dest = format!("{}:{}", destination_ip, destination_port); 62 | 63 | loop { 64 | match rx.recv() { 65 | Ok(l) => { 66 | udp.send_to(l.as_bytes(), &*dest).unwrap(); 67 | }, 68 | Err(e) => { panic!(e) } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/outputs/stdout.rs: -------------------------------------------------------------------------------- 1 | use processor::{OutputProcessor, ConfigurableFilter}; 2 | 3 | use std::collections::HashMap; 4 | use std::sync::mpsc::Receiver; 5 | use std::thread::JoinHandle; 6 | 7 | /// # Stdout output 8 | /// 9 | /// - sends output to stdout 10 | /// 11 | /// ### catapult.conf 12 | /// 13 | /// ``` 14 | /// output { 15 | /// stdout 16 | /// } 17 | /// ``` 18 | /// ### Parameters 19 | /// 20 | /// - none 21 | 22 | 23 | pub struct Stdout { 24 | name: String 25 | } 26 | 27 | impl Stdout { 28 | pub fn new(name: String) -> Stdout { 29 | Stdout{ name: name } 30 | } 31 | } 32 | 33 | impl ConfigurableFilter for Stdout { 34 | fn human_name(&self) -> &str { 35 | self.name.as_ref() 36 | } 37 | 38 | } 39 | 40 | impl OutputProcessor for Stdout { 41 | fn start(&self, rx: Receiver, config: &Option>) -> Result, String> { 42 | self.invoke(rx, config, Stdout::handle_func) 43 | } 44 | fn handle_func(rx: Receiver, _config: Option>) { 45 | loop { 46 | match rx.recv() { 47 | Ok(l) => { println!("{}", l) } 48 | Err(e) => { panic!(e) } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/processor.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::mpsc::{Receiver, SyncSender}; 3 | use std::thread; 4 | use std::sync::mpsc::sync_channel; 5 | use std::thread::JoinHandle; 6 | 7 | pub trait ConfigurableFilter { 8 | fn human_name(&self) -> &str; 9 | fn mandatory_fields(&self) -> Vec<&str> { 10 | vec![] 11 | } 12 | 13 | fn requires_fields(&self, optional_config: &Option>, required_fields: Vec<&str>) { 14 | let mut missing_fields = Vec::new(); 15 | match optional_config { 16 | &Some(ref config) => { 17 | for required in required_fields { 18 | if !config.contains_key(required) { 19 | missing_fields.push(required); 20 | } 21 | } 22 | }, 23 | &None => {missing_fields.extend(&required_fields)} 24 | } 25 | 26 | if missing_fields.len() > 0 { 27 | panic!("Missing fields for \"{}\": {:?}", self.human_name(), missing_fields); 28 | } 29 | } 30 | } 31 | 32 | pub trait InputProcessor: ConfigurableFilter { 33 | #[allow(unused_variables)] 34 | fn start(&self, config: &Option>) -> Receiver { 35 | panic!("Not implemented"); 36 | } 37 | 38 | #[allow(unused_variables)] 39 | fn handle_func(tx: SyncSender, config: Option>) { 40 | panic!("Not implemented"); 41 | } 42 | 43 | fn invoke(&self, config: &Option>, 44 | handle_func: fn(tx: SyncSender, config: Option>)) -> Receiver 45 | { 46 | let (tx, rx) = sync_channel(10000); 47 | let conf = config.clone(); 48 | 49 | let run_loop = thread::Builder::new().name("run_loop".to_string()).spawn(move || { 50 | handle_func(tx, conf); 51 | }); 52 | 53 | match run_loop { 54 | Ok(_) => { 55 | println!("Started Thread for {}", self.human_name()); 56 | rx 57 | }, 58 | Err(e) => panic!("Unable to spawn {} input thread: {}", self.human_name(), e) 59 | } 60 | } 61 | } 62 | 63 | pub trait OutputProcessor: ConfigurableFilter { 64 | fn start(&self, _rx: Receiver, _config: &Option>) -> Result, String> { 65 | panic!("Not implemented"); 66 | } 67 | 68 | #[allow(unused_variables)] 69 | fn handle_func(rx: Receiver, config: Option>) { 70 | panic!("Not implemented"); 71 | } 72 | 73 | fn invoke(&self, rx: Receiver, config: &Option>, 74 | handle_func: fn(rx: Receiver, config: Option>)) -> Result, String> 75 | { 76 | let conf = config.clone(); 77 | 78 | let run_loop = thread::Builder::new().name("run_loop".to_string()).spawn(move || { 79 | handle_func(rx, conf); 80 | }); 81 | 82 | match run_loop { 83 | Ok(jh) => Ok(jh), 84 | Err(e) => Err(format!("Unable to spawn {} output thread: {}", self.human_name(), e)) 85 | } 86 | } 87 | } 88 | --------------------------------------------------------------------------------