├── LICENSE ├── README.md ├── slack.c └── uwsgiplugin.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 unbit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uWSGI-slack 2 | uWSGI-slack is a plugin that allows alarms/hooks integration with the [Slack](https://www.slack.com/) service. 3 | 4 | ## Features 5 | uWSGI-slack provides the following features: 6 | 7 | * Registers _slack alarm_ and _slack hook_. 8 | * Support to the [Slack incoming webhooks API](https://api.slack.com/incoming-webhooks) (send messages to channels, users and so on). 9 | * Support to the [Slack messagge attachments API](https://api.slack.com/docs/attachments) (fancy message attachments). 10 | 11 | ## Installation 12 | This plugin requires: 13 | * [libcurl](http://curl.haxx.se/libcurl/) (to send HTTP requests with ease) 14 | * [jansson](https://github.com/akheron/jansson) (for JSON parsing) 15 | 16 | Please follow the specific documentation on how to install them. 17 | 18 | This plugin is 2.0 friendly. 19 | You can thus build it with: 20 | ```bash 21 | $ git clone https://github.com/aldur/uwsgi-slack 22 | $ uwsgi --build-plugin uwsgi-slack 23 | ``` 24 | 25 | ## Configuration 26 | To use this plugin you'll need to setup an [incoming webhook integration](https://my.slack.com/services/new/incoming-webhook/) in your Slack team. 27 | 28 | You can configure the alarms in your app as follows: 29 | ```ini 30 | [uwsgi] 31 | plugins = slack 32 | 33 | ; register a 'slackme' alarm 34 | alarm = slackme slack:webhook_url=YOUR_SLACK_TEAM_WEBHOOK_URL,username=uWSGI Alarmer,icon_emoji=:heavy_exclamation_mark: 35 | ; raise alarms no more than 1 time per minute (default is 3 seconds) 36 | alarm-freq = 60 37 | 38 | ; raise an alarm whenever uWSGI segfaults 39 | alarm-segfault = slackme 40 | 41 | ; raise an alarm whenever /danger is hit 42 | route = ^/danger alarm:slackme /danger has been visited !!! 43 | 44 | ; raise an alarm when the avergae response time is higher than 3000 milliseconds 45 | metric-alarm = key=worker.0.avg_response_time,value=3000,alarm=slackme 46 | 47 | ; ... 48 | ``` 49 | The only mandatory key-value field we need to send an alarm is the `webhook_url`. 50 | In the previous example we've also set the Slack BOT username and it's icon (an emoji). 51 | 52 | Hooks, on the other side, require the message text too: 53 | ```ini 54 | [uwsgi] 55 | plugins = slack 56 | 57 | hook-post-app = slack:webhook_url=YOUR_SLACK_TEAM_WEBHOOK_URL,text=Your awesome app has just been loaded!,username=Your friendly neighbourhood Flip-Man,icon_emoji=:flipper: 58 | 59 | ; ... 60 | ``` 61 | 62 | ### Alarms/hooks key-value options 63 | We support the entire [Slack incoming webhook API](https://api.slack.com/incoming-webhooks). 64 | Available key-values options are: 65 | 66 | * text 67 | * channel 68 | * username 69 | * icon_emoji 70 | * icon_url 71 | 72 | Unspecified settings will fallback to the Slack webhook defaults. 73 | 74 | As an extra, you can specify a `;` separated list of [attachments](#attachments) as a value for the `attachments` key. 75 | 76 | ### Attachments 77 | You can define an attachment and send messages containing it by using the following configuration snippet: 78 | 79 | ```ini 80 | [uwsgi] 81 | plugins = slack 82 | 83 | slack-attachment = name=groove,title=A slack attachment,color=#7CD197 84 | hook-post-app = slack:webhook_url=YOUR_SLACK_TEAM_WEBHOOK_URL,attachments=groove,text=Hook text 85 | 86 | ; ... 87 | ``` 88 | 89 | As you can see we specify a mandatory attachment `name`, we setup the attachment and we link it to the uWSGI hook / alarm through the `attachments` key. 90 | 91 | __Note:__ the attachments' name lookup is done at runtime. A malformed attachment name will let uWSGI ignore the whole alarm / hook trigger. 92 | 93 | Attachments key-value options are: 94 | 95 | * name (_mandatory_) 96 | * fallback 97 | * color 98 | * pretext 99 | * author_name 100 | * author_link 101 | * author_icon 102 | * title 103 | * title_link 104 | * text 105 | * image_url 106 | * thumb_url 107 | 108 | A detailed explanation for each key can be found [here](https://api.slack.com/docs/attachments). 109 | Again, unspecified settings will fallback to Slack defaults. 110 | 111 | As an extra, you can link a `;` separated list of [fields](#fields) to the attachment by using the `fields` key. 112 | 113 | ### Fields 114 | Fields are nested within attachments and will be displayed in a table. 115 | 116 | Their configuration is should look familiar: 117 | ```ini 118 | [uwsgi] 119 | plugins = slack 120 | 121 | slack-field = name=project,title=Project,value=Awesome Project,short=true 122 | slack-field = name=environment,title=Environment,value=Production Project 123 | 124 | slack-attachment = name=groove,title=A slack attachment,color=#7CD197,fields=project;environment 125 | 126 | hook-post-app = slack:webhook_url=YOUR_SLACK_TEAM_WEBHOOK_URL,attachments=groove,text=Hook text 127 | 128 | ; ... 129 | ``` 130 | 131 | Fields key-value options are: 132 | 133 | * name (_mandatory_) 134 | * title 135 | * value 136 | * short (if anything is set will be considered True, False otherwise) 137 | 138 | As usual, better documentation for each key can be found [here](https://api.slack.com/docs/attachments). 139 | 140 | ### uWSGI related key-value options 141 | On the uWSGI/networking side you can set: 142 | * timeout: specifies the socket timeout. 143 | * ssl_no_verify: tells Curl to not verify the server SSL certificate. 144 | 145 | ## Screens 146 | An alarm-resulting example with attachments and fields in action. 147 | 148 | ![Example](http://i.imgur.com/VZd2auX.png) 149 | -------------------------------------------------------------------------------- /slack.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #define PNAME "slack" 6 | #define LNAME "[uwsgi-"PNAME"]" 7 | 8 | extern struct uwsgi_server uwsgi; 9 | 10 | struct uwsgi_slack { 11 | struct uwsgi_string_list *attachment_s; 12 | struct slack_attachment *attachments; 13 | 14 | struct uwsgi_string_list *field_s; 15 | struct slack_field *fields; 16 | } uslack; 17 | 18 | struct slack_config { 19 | // Slack Incoming Webhook URL 20 | char *webhook_url; 21 | 22 | // Content 23 | char *text; 24 | 25 | // Destination 26 | char *channel; 27 | 28 | // Appearance 29 | char *username; 30 | char *icon_emoji; 31 | char *icon_url; 32 | 33 | // Attachments support 34 | char *attachments; 35 | 36 | // Other configuration options 37 | char *ssl_no_verify; 38 | char *timeout; 39 | }; 40 | 41 | struct slack_attachment { 42 | char *name; 43 | char *fallback; 44 | char *color; 45 | char *pretext; 46 | 47 | char *author_name; 48 | char *author_link; 49 | char *author_icon; 50 | 51 | char *title; 52 | char *title_link; 53 | 54 | char *text; 55 | 56 | char *fields; 57 | 58 | char *image_url; 59 | char *thumb_url; 60 | 61 | struct slack_attachment *next; 62 | }; 63 | 64 | struct slack_field { 65 | char *name; 66 | 67 | char *title; 68 | char *value; 69 | char sshort; 70 | 71 | struct slack_field *next; 72 | }; 73 | 74 | static struct uwsgi_option slack_options[] = { 75 | {"slack-attachment", required_argument, 0, 76 | "specify a slack attachment", uwsgi_opt_add_string_list, &uslack.attachment_s, 0}, 77 | {"slack-field", required_argument, 0, 78 | "specify a slack attachment-field", uwsgi_opt_add_string_list, &uslack.field_s, 0}, 79 | UWSGI_END_OF_OPTIONS 80 | }; 81 | 82 | #define sfkv(x) #x, &f->x 83 | static void slack_add_field(char *arg, size_t arg_len) { 84 | struct slack_field *f = uwsgi_calloc(sizeof(struct slack_field)); 85 | 86 | char *sshort = NULL; 87 | if (uwsgi_kvlist_parse(arg, arg_len, ',', '=', 88 | sfkv(name), 89 | sfkv(title), 90 | sfkv(value), 91 | "short", &sshort, 92 | NULL) 93 | ){ 94 | uwsgi_log(LNAME" unable to parse slack field definition\n"); 95 | goto shutdown; 96 | } 97 | 98 | if (!f->name) { 99 | uwsgi_log(LNAME" a name is required for the field definition\n"); 100 | goto shutdown; 101 | } 102 | 103 | if (!f->title) { 104 | uwsgi_log(LNAME" a title is required for the field definition\n"); 105 | goto shutdown; 106 | } 107 | 108 | if (!f->value) { 109 | uwsgi_log(LNAME" a value is required for the field definition\n"); 110 | goto shutdown; 111 | } 112 | 113 | if (sshort) { 114 | f->sshort = 1; 115 | free(sshort); 116 | } 117 | 118 | struct slack_field *uslf = uslack.fields; 119 | if (!uslf) { 120 | uslack.fields = f; 121 | } else { 122 | while (uslf->next) { 123 | uslf = uslf->next; 124 | } 125 | 126 | uslf->next = f; 127 | } 128 | 129 | return; 130 | 131 | shutdown: 132 | exit(1); 133 | } 134 | 135 | static struct slack_field *slack_get_field(char *field_name) { 136 | struct slack_field *sfield = uslack.fields; 137 | 138 | while (sfield) { 139 | if (strcmp(sfield->name, field_name) == 0) { 140 | return sfield; 141 | } 142 | 143 | sfield = sfield->next; 144 | } 145 | 146 | return NULL; 147 | } 148 | 149 | static struct slack_attachment *slack_get_attachment(char *attachment_name) { 150 | struct slack_attachment *sattachment = uslack.attachments; 151 | 152 | while (sattachment) { 153 | if (strcmp(sattachment->name, attachment_name) == 0) { 154 | return sattachment; 155 | } 156 | 157 | sattachment = sattachment->next; 158 | } 159 | 160 | return NULL; 161 | } 162 | 163 | #define sakv(x) #x, &attachment->x 164 | static void slack_add_attachment(char *arg, size_t arg_len) { 165 | struct slack_attachment *attachment = uwsgi_calloc(sizeof(struct slack_attachment)); 166 | 167 | if (uwsgi_kvlist_parse(arg, arg_len, ',', '=', 168 | sakv(name), 169 | sakv(fallback), 170 | sakv(color), 171 | sakv(pretext), 172 | sakv(author_name), 173 | sakv(author_link), 174 | sakv(author_icon), 175 | sakv(title), 176 | sakv(title_link), 177 | sakv(text), 178 | sakv(fields), 179 | sakv(image_url), 180 | sakv(thumb_url), 181 | NULL) 182 | ){ 183 | uwsgi_log(LNAME" unable to parse slack attachment definition\n"); 184 | goto shutdown; 185 | } 186 | 187 | if (!attachment->name) { 188 | uwsgi_log(LNAME" a name is required for the attachment definition\n"); 189 | goto shutdown; 190 | } 191 | 192 | struct slack_attachment *uslas = uslack.attachments; 193 | if (!uslas) { 194 | uslack.attachments = attachment; 195 | } else { 196 | while (uslas->next) { 197 | uslas = uslas->next; 198 | } 199 | 200 | uslas->next = attachment; 201 | } 202 | 203 | return; 204 | 205 | shutdown: 206 | exit(1); 207 | } 208 | 209 | #define pkv(x) #x, &pbc->x 210 | static int slack_config_do(char *arg, struct slack_config *pbc) { 211 | memset(pbc, 0, sizeof(struct slack_config)); 212 | 213 | if (uwsgi_kvlist_parse(arg, strlen(arg), ',', '=', 214 | pkv(webhook_url), 215 | pkv(text), 216 | pkv(channel), 217 | pkv(username), 218 | pkv(icon_emoji), 219 | pkv(icon_url), 220 | pkv(ssl_no_verify), 221 | pkv(timeout), 222 | pkv(timeout), 223 | pkv(attachments), 224 | NULL)) { 225 | uwsgi_log(LNAME" unable to parse specified Slack options\n"); 226 | return -1; 227 | } 228 | 229 | if (!pbc->webhook_url) { 230 | uwsgi_log(LNAME" you need to specify a Slack Webhook URL\n"); 231 | return -1; 232 | } 233 | 234 | return 0; 235 | } 236 | 237 | #define sfree(x) if(pbc->x) free(pbc->x) 238 | static void slack_free(struct slack_config *pbc) { 239 | sfree(webhook_url); 240 | sfree(text); 241 | sfree(channel); 242 | sfree(username); 243 | sfree(attachments); 244 | sfree(icon_emoji); 245 | sfree(icon_url); 246 | sfree(ssl_no_verify); 247 | sfree(timeout); 248 | } 249 | 250 | static json_t *build_field_json(struct slack_field *f) { 251 | json_t *jf = json_object(); 252 | 253 | if (json_object_set_new(jf, "title", json_string(f->title))) goto error; 254 | if (json_object_set_new(jf, "value", json_string(f->value))) goto error; 255 | 256 | if (f->sshort) { 257 | if (json_object_set_new(jf, "short", json_true())) goto error; 258 | } 259 | 260 | return jf; 261 | 262 | error: 263 | uwsgi_log(LNAME" error while building Slack field JSON\n"); 264 | return NULL; 265 | } 266 | 267 | #define saj(x) if (a->x && json_object_set_new(ja, #x, json_string(a->x))) goto error 268 | static json_t *build_attachment_json(struct slack_attachment *a) { 269 | json_t *ja = json_object(); 270 | 271 | saj(fallback); 272 | saj(color); 273 | saj(pretext); 274 | saj(author_name); 275 | saj(author_link); 276 | saj(author_icon); 277 | saj(title); 278 | saj(title_link); 279 | saj(text); 280 | saj(image_url); 281 | saj(thumb_url); 282 | 283 | if (a->fields) { 284 | // We're gonna fill the attachment fields 285 | json_t *jfs = json_array(); 286 | char *field_name = uwsgi_concat2n(a->fields, strlen(a->fields), "", 0); 287 | char *field_name_f = field_name; 288 | 289 | char *semicolon; 290 | do { 291 | // Required field-names are divided by a semicolon 292 | semicolon = strchr(field_name, ';'); 293 | 294 | char *field_name_t = field_name; 295 | if (semicolon) { 296 | *semicolon = 0; 297 | field_name = ++semicolon; 298 | } 299 | 300 | struct slack_field *f; 301 | if (!(f = slack_get_field(field_name_t))) { 302 | uwsgi_log(LNAME" unable to find required attachment-field name\n"); 303 | goto error; 304 | } 305 | 306 | json_t *jf = build_field_json(f); 307 | if (!jf || json_array_append_new(jfs, jf)) goto error; 308 | } while (semicolon); 309 | 310 | if (json_object_set_new(ja, "fields", jfs)) goto error; 311 | free(field_name_f); 312 | } 313 | 314 | return ja; 315 | 316 | error: 317 | uwsgi_log(LNAME" error while building Slack attachment JSON\n"); 318 | return NULL; 319 | } 320 | 321 | #define sj(x) if (pbc->x && json_object_set_new(j, #x, json_string(pbc->x))) goto error 322 | static json_t *build_json(struct slack_config *pbc, char* text) { 323 | json_t *j = json_object(); 324 | 325 | if (text) { 326 | if (json_object_set_new(j, "text", json_string(text))) goto error; 327 | } else { 328 | if (json_object_set_new(j, "text", json_string(pbc->text))) goto error; 329 | } 330 | 331 | sj(channel); 332 | sj(username); 333 | 334 | if (pbc->icon_emoji) { 335 | if (json_object_set_new(j, "icon_emoji", json_string(pbc->icon_emoji))) goto error; 336 | } else if (pbc->icon_url) { 337 | if (json_object_set_new(j, "icon_url", json_string(pbc->icon_url))) goto error; 338 | } 339 | 340 | // We'll handle the attachments here. 341 | if (pbc->attachments) { 342 | char *attachments = uwsgi_concat2n(pbc->attachments, strlen(pbc->attachments), "", 0); 343 | char *attachments_f = attachments; 344 | 345 | json_t *jas = json_array(); 346 | 347 | char *semicolon; 348 | do { 349 | // Required attachment-names are divided by a semicolon 350 | semicolon = strchr(attachments, ';'); 351 | 352 | char *attachments_t = attachments; 353 | if (semicolon) { 354 | *semicolon = 0; 355 | attachments = ++semicolon; 356 | } 357 | 358 | struct slack_attachment *a; 359 | if (!(a = slack_get_attachment(attachments_t))) { 360 | uwsgi_log(LNAME" unable to find required attachment name %s\n", 361 | attachments_t); 362 | goto error; 363 | } 364 | 365 | json_t *ja = build_attachment_json(a); 366 | if (!ja || json_array_append_new(jas, ja)) goto error; 367 | 368 | } while (semicolon); 369 | 370 | if (json_object_set_new(j, "attachments", jas)) goto error; 371 | free(attachments_f); 372 | } 373 | 374 | return j; 375 | 376 | error: 377 | uwsgi_log(LNAME" error while building Slack JSON\n"); 378 | return NULL; 379 | } 380 | 381 | // Callback to ignore response from server 382 | size_t write_data(void *buffer, size_t size, size_t nmemb, void *userp) { 383 | return size * nmemb; 384 | } 385 | 386 | static int slack_request(struct slack_config *pbc, char* text) { 387 | int ret = 0; 388 | char *j_string = NULL; 389 | 390 | struct curl_slist *headerlist = NULL; 391 | static const char *content_type = "Content-Type: application/json"; 392 | 393 | CURL *curl = curl_easy_init(); 394 | if (!curl) { 395 | uwsgi_log(LNAME" curl_easy_init error\n"); 396 | ret = -1; 397 | goto cleanup; 398 | } 399 | 400 | // Prepare the header 401 | headerlist = curl_slist_append(headerlist, content_type); 402 | 403 | // Prepare the text 404 | json_t *j; 405 | if (!(j = build_json(pbc, text))) { 406 | ret = -1; 407 | goto cleanup; 408 | } 409 | 410 | j_string = json_dumps(j, 0); 411 | json_decref(j); 412 | 413 | int timeout = uwsgi.socket_timeout; 414 | if (pbc->timeout) { 415 | char *end; 416 | int t = strtol(pbc->timeout, &end, 10); 417 | if (!*end) { 418 | timeout = t; 419 | } 420 | } 421 | char *url = pbc->webhook_url; 422 | 423 | curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout); 424 | curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, timeout); 425 | curl_easy_setopt(curl, CURLOPT_URL, url); 426 | 427 | if (pbc->ssl_no_verify) { 428 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); 429 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); 430 | } 431 | 432 | // CURLOPT_POSTFIELDS sets the request method to POST 433 | // And the payload to the json string 434 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, j_string); 435 | 436 | // Set the headers after the HTTP method to overwrite Content Type 437 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerlist); 438 | 439 | // Set a custom writer to avoid SDOUT 440 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data); 441 | 442 | CURLcode res = curl_easy_perform(curl); 443 | long http_code = 0; 444 | curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &http_code); 445 | 446 | if (res != CURLE_OK) { 447 | uwsgi_log(LNAME" libcurl error: %s\n", curl_easy_strerror(res)); 448 | ret = -1; 449 | goto cleanup; 450 | } else if (http_code != 200){ 451 | uwsgi_log(LNAME" Slack returned wrong HTTP status code: %d\n", http_code); 452 | ret = -1; 453 | goto cleanup; 454 | } 455 | 456 | cleanup: 457 | curl_easy_cleanup(curl); 458 | curl_slist_free_all(headerlist); 459 | 460 | if (j_string) { 461 | free(j_string); 462 | } 463 | 464 | return ret; 465 | } 466 | 467 | static int slack_hook(char *arg) { 468 | int ret = -1; 469 | struct slack_config pbc; 470 | 471 | if (slack_config_do(arg, &pbc)) { 472 | goto clear; 473 | } 474 | 475 | if (!pbc.text) { 476 | uwsgi_log(LNAME" you need to specify the Slack message text for hooks\n"); 477 | goto clear; 478 | } 479 | 480 | ret = slack_request(&pbc, NULL); 481 | 482 | clear: 483 | slack_free(&pbc); 484 | return ret; 485 | } 486 | 487 | static void slack_alarm_func(struct uwsgi_alarm_instance *uai, char *msg, size_t len) { 488 | struct slack_config *pbc = (struct slack_config *)uai->data_ptr; 489 | 490 | char *text = uwsgi_concat2n(msg, len, "", 0); 491 | slack_request(pbc, text); 492 | free(text); 493 | } 494 | 495 | static void slack_alarm_init(struct uwsgi_alarm_instance *uai) { 496 | struct slack_config *pbc = uwsgi_calloc(sizeof(struct slack_config)); 497 | 498 | if (slack_config_do(uai->arg, pbc)) { 499 | exit(1); 500 | } 501 | 502 | uai->data_ptr = pbc; 503 | } 504 | 505 | static void slack_register() { 506 | uwsgi_register_hook(PNAME, slack_hook); 507 | uwsgi_register_alarm(PNAME, slack_alarm_init, slack_alarm_func); 508 | } 509 | 510 | static int slack_init() { 511 | struct uwsgi_string_list *usl = uslack.field_s; 512 | while (usl) { 513 | slack_add_field(usl->value, usl->len); 514 | usl = usl->next; 515 | } 516 | 517 | usl = uslack.attachment_s; 518 | while (usl) { 519 | slack_add_attachment(usl->value, usl->len); 520 | usl = usl->next; 521 | } 522 | 523 | return 0; 524 | } 525 | 526 | struct uwsgi_plugin slack_plugin = { 527 | .name = PNAME, 528 | .init = slack_init, 529 | .options = slack_options, 530 | .on_load = slack_register, 531 | }; 532 | -------------------------------------------------------------------------------- /uwsgiplugin.py: -------------------------------------------------------------------------------- 1 | NAME = 'slack' 2 | LIBS = ['-lcurl', '-ljansson'] 3 | GCC_LIST = ['slack'] 4 | --------------------------------------------------------------------------------