1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
//! Integration API
//!
//! The PagerDuty event integration API is how you would add PagerDuty's advanced alerting
//! functionality to any system that can make an HTTP API call. You can now add phone, SMS and email
//! alerting to your monitoring tools, ticketing systems and custom software.
//!
//! # Description
//!
//! The API was designed to allow you to easily integrate a monitoring system with a Service in
//! PagerDuty. Monitoring systems generally send out events when problems are detected and when
//! these problems have been resolved (fixed). Some more advanced systems also understand the
//! concept of acknowledgements: problems can be acknowledged by an engineer to signal he or she is
//! working on fixing the issue.
//!
//! Since monitoring systems emit events, the API is based around accepting events. Incoming events
//! (sent via the API) are routed to a PagerDuty service and processed. They may result in a new
//! incident being created, or an existing incident being acknowledged or resolved.
//!
//! The same event-based API can also be used to integrate a PagerDuty service with ticketing
//! systems and various other software tools.
//!
//! # API Limits
//!
//! There is a limit on the number of events that a service can accept at any given time. Depending
//! on the behavior of the incoming traffic and how many incidents are being created at once, we
//! reduce our throttle dynamically.
//!
//! If each of the events your monitoring system is sending is important, be sure to retry on a 403
//! response code, preferably with a back off.
//!
//! # Response codes and Retry Logic
//!
//! Ideally, the API request will succeed and the PagerDuty server will indicate that it
//! successfully received that event. In practice, the request may fail due to various reasons.
//!
//! The following table shows the possible results of the API request and if you need to retry the
//! API call for that result:
//!
//! | Result           | Description                                                                                   | Retry?                       |
//! |------------------|-----------------------------------------------------------------------------------------------|------------------------------|
//! | 200              | OK - The event has been accepted by PagerDuty. See below for details.                         | No                           |
//! | 400              | Bad Request - Check that the JSON is valid. See below for details.                            | No                           |
//! | 403              | Forbidden - Too many API calls at a time.                                                     | Yes - retry after some time. |
//! | 5xx              | Internal Server Error - the PagerDuty server experienced an error while processing the event. | Yes - retry after some time. |
//! | Networking Error | Error while trying to communicate with PagerDuty servers.                                     | Yes - retry after some time. |
//!

use std::borrow::Cow;

use hyper::header::Headers;
use hyper::method::Method;
use hyper::status::StatusCode;

use serde::Serialize;
use serde_json::{from_str, to_string, to_value, Value as Json};

use AuthToken;
use request::{self, Requestable};

/// Event to report a new or ongoing problem.
///
/// When PagerDuty receives a trigger event, it will either open a new incident, or add
/// a new trigger log entry to an existing incident, depending on the provided incident_key.
#[derive(Debug, Serialize)]
pub struct TriggerEvent<'a> {
    service_key: Cow<'a, str>,

    event_type: &'static str,

    description: Cow<'a, str>,

    #[serde(skip_serializing_if="Option::is_none")]
    incident_key: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if="Option::is_none")]
    client: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if="Option::is_none")]
    client_url: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if="Option::is_none")]
    details: Option<Json>,

    #[serde(skip_serializing_if="Vec::is_empty")]
    contexts: Vec<Context<'a>>,
}

impl<'a> TriggerEvent<'a> {
    /// Create a new trigger event payload
    ///
    /// service_key: The GUID of one of your "Generic API" services. This is the "service key"
    /// listed on a Generic API's service detail page.
    ///
    /// description: A short description of the problem that led to this trigger. This field (or a
    /// truncated version) will be used when generating phone calls, SMS messages and alert emails.
    /// It will also appear on the incidents tables in the PagerDuty UI. The maximum length is 1024
    /// characters.
    pub fn new<S>(service_key: S, description: S) -> Self
        where S: Into<Cow<'a, str>>
    {
        TriggerEvent {
            service_key: service_key.into(),
            event_type: "trigger",
            description: description.into(),
            incident_key: None,
            client: None,
            client_url: None,
            details: None,
            contexts: Vec::new(),
        }
    }

    /// Set incident_key
    ///
    /// Identifies the incident to which this trigger event should be applied. If there's no open
    /// (i.e. unresolved) incident with this key, a new one will be created. If there's already an
    /// open incident with a matching key, this event will be appended to that incident's log. The
    /// event key provides an easy way to "de-dup" problem reports.
    pub fn set_incident_key<S>(mut self, incident_key: S) -> Self
        where S: Into<Cow<'a, str>>
    {
        self.incident_key = Some(incident_key.into());
        self
    }

    /// Set event's client
    ///
    /// The name of the monitoring client that is triggering this event.
    pub fn set_client<S>(mut self, client: S) -> Self
        where S: Into<Cow<'a, str>>
    {
        self.client = Some(client.into());
        self
    }

    /// Set event's client_url
    ///
    /// The URL of the monitoring client that is triggering this event.
    pub fn set_client_url<S>(mut self, client_url: S) -> Self
        where S: Into<Cow<'a, str>>
    {
        self.client_url = Some(client_url.into());
        self
    }

    /// Set event details
    ///
    /// An arbitrary JSON object containing any data you'd like included in the incident log.
    ///
    /// # Examples
    /// ```no_run
    /// # #![feature(custom_derive, plugin)]
    /// # #![plugin(serde_macros)]
    /// #
    /// # extern crate serde;
    /// # extern crate pagerduty;
    /// #
    /// # use pagerduty::integration::TriggerEvent;
    /// // Extra data to be included with the event. Anything that implements
    /// // Serialize can be passed to `set_details`.
    /// #[derive(Serialize)]
    /// struct Details {
    ///     what: &'static str,
    ///     count: i32,
    /// }
    ///
    /// # fn main() {
    /// // Create a trigger event and include custom data
    /// TriggerEvent::new("service_key", "event description")
    ///     .set_details(&Details {
    ///          what: "Server fire",
    ///          count: 1,
    ///     });
    /// # }
    ///
    /// ```
    pub fn set_details<T: ?Sized>(mut self, details: &T) -> Self
        where T: Serialize
    {
        self.details = Some(to_value(details));
        self
    }

    /// Add a Context to this event
    ///
    /// Contexts to be included with the incident trigger such as links to graphs or images. A
    /// "type" is required for each context submitted. For type "link", an "href" is required. You
    /// may optionally specify "text" with more information about the link. For type "image", "src"
    /// must be specified with the image src. You may optionally specify an "href" or an "alt" with
    /// this image.
    pub fn add_context(mut self, context: Context<'a>) -> Self {
        self.contexts.push(context);
        self
    }
}

/// An informational asset attached to the incident
///
/// This Context type is really a union of two different types, Image and Link. Due to object safety
/// issues, it's not possible to have a Context trait that can be serialized with Serde.
///
/// In the case that Context is an image, it must have a `src` attribute and may optionally have an
/// `href` and `alt` attributes. In the case of a link, context must have `href` and may optionally
/// include `text. To enforce these invariants, all of the fields are kept private, and all of the
/// properties must be specifed at once using the `link` and `image` methods.
#[derive(Debug, Serialize)]
pub struct Context<'a> {
    /// The type of context being attached to the incident. This will be a "link" or "image".
    #[serde(rename = "type")]
    context_type: &'static str,

    /// The source of the image being attached to the incident. This image must be served via HTTPS.
    #[serde(skip_serializing_if="Option::is_none")]
    src: Option<Cow<'a, str>>,

    /// Optional link for the image OR The link being attached to the incident.
    #[serde(skip_serializing_if="Option::is_none")]
    href: Option<Cow<'a, str>>,

    /// Optional alternative text for the image.
    #[serde(skip_serializing_if="Option::is_none")]
    alt: Option<Cow<'a, str>>,

    /// Optional information pertaining to the incident.
    #[serde(skip_serializing_if="Option::is_none")]
    text: Option<Cow<'a, str>>,
}

impl<'a> Context<'a> {
    /// Create a `link` context object
    pub fn link<S>(href: S, text: S) -> Context<'a>
        where S: Into<Cow<'a, str>>
    {
        Context {
            context_type: "link",
            href: Some(href.into()),
            text: Some(text.into()),
            alt: None,
            src: None,
        }
    }

    /// Create an `image` context object
    pub fn image<S>(src: S, href: Option<S>, alt: Option<S>) -> Context<'a>
        where S: Into<Cow<'a, str>>
    {
        Context {
            context_type: "image",
            src: Some(src.into()),
            href: href.map(|s| s.into()),
            alt: alt.map(|s| s.into()),
            text: None,
        }
    }
}

macro_rules! shared_event_type {
    { $(#[$attr:meta])* name => $name:ident; event_type => $event_type:expr } => {

        $(#[$attr])*
        #[derive(Debug, Serialize)]
        pub struct $name<'a> {
            service_key: Cow<'a, str>,
            event_type: &'static str,
            incident_key: Cow<'a, str>,

            #[serde(skip_serializing_if="Option::is_none")]
            description: Option<Cow<'a, str>>,

            #[serde(skip_serializing_if="Option::is_none")]
            details: Option<Json>,
        }

        impl<'a> $name<'a> {
            /// Create a new event
            ///
            /// * **service_key**: The GUID of one of your "Events API" services. This is the
            /// "service key" listed on a Generic API's service detail page.
            ///
            /// * **incident_key**: Identifies the incident to resolve. This should be the
            /// `incident_key` you received back when the incident was first opened by a trigger
            /// event. Resolve events referencing resolved or nonexistent incidents will be
            /// discarded.
            pub fn new<S>(service_key: S, incident_key: S) -> Self
                where S: Into<Cow<'a, str>>
            {
                $name {
                    service_key: service_key.into(),
                    event_type: $event_type,
                    incident_key: incident_key.into(),
                    description: None,
                    details: None,
                }
            }

            /// Set event details
            ///
            /// An arbitrary JSON object containing any data you'd like included in the incident
            /// log.
            ///
            /// For an example, please see the similar
            /// [`TriggerEvent::set_details`](struct.TriggerEvent.html#method.set_details).
            pub fn set_details<T: ?Sized>(mut self, details: &T) -> Self
                where T: Serialize
            {
                self.details = Some(to_value(details));
                self
            }

            /// Set text that will appear in the incident's log associated with this event.
            pub fn set_description<S>(mut self, description: S) -> Self
                where S: Into<Cow<'a, str>>
            {
                self.description = Some(description.into());
                self
            }
        }

        impl<'a> Requestable for $name<'a> {
            type Response = Response;

            fn body(&self) -> String {
                to_string(&self).unwrap()
            }

            fn method(&self) -> Method {
                Method::Post
            }

            fn get_response(status: StatusCode,
                            headers: &Headers,
                            body: &str) -> request::Result<Response> {
                Response::get_response(status, headers, body)
            }
        }


    }
}

shared_event_type! {
    /// Cause the referenced incident to enter the resolved state.
    ///
    /// Once an incident is resolved, it won't generate any additional notifications. New trigger
    /// events with the same incident_key as a resolved incident won't re-open the incident.
    /// Instead, a new incident will be created. Your monitoring tools should send PagerDuty a
    /// resolve event when the problem that caused the initial trigger event has been fixed.
    name => ResolveEvent; event_type => "resolve"
}

shared_event_type! {
    /// Acknowledge events cause the referenced incident to enter the acknowledged state.
    ///
    /// While an incident is acknowledged, it won't generate any additional notifications, even if
    /// it receives new trigger events. Your monitoring tools should send PagerDuty an acknowledge
    /// event when they know someone is presently working on the problem.
    name => AcknowledgeEvent; event_type => "acknowledge"
}

/// Response types from the integration API
pub mod response {
    /// If the request is invalid, PagerDuty will respond with HTTP code 400 and this object
    #[derive(Debug, Deserialize, PartialEq, Eq)]
    pub struct BadRequest {
        /// invalid event
        pub status: String,

        /// A description of the problem
        pub message: String,

        /// An array of specific error messages
        pub errors: Vec<String>,
    }

    /// If the request is well-formatted, PagerDuty will respond with HTTP code 200 and this object.
    #[derive(Debug, Deserialize, PartialEq, Eq)]
    pub struct Success {
        /// The string _"success"_
        pub status: String,

        /// Event processed
        pub message: String,

        /// The key of the incident that will be affected by the request.
        pub incident_key: String,
    }
}

/// A Response from the integration API
///
/// A union of all possible responses for the integration API.
#[derive(Debug, PartialEq, Eq)]
pub enum Response {
    Success(response::Success),
    BadRequest(response::BadRequest),
    Forbidden,
    InternalServerError,
}

impl Response {
    fn get_response(status: StatusCode,
                    _headers: &Headers,
                    body: &str) -> request::Result<Response> {
        match status {
            StatusCode::Ok => {
                let res: response::Success = try!(from_str(body));
                Ok(Response::Success(res))
            },
            StatusCode::BadRequest => {
                let res: response::BadRequest = try!(from_str(body));
                Ok(Response::BadRequest(res))
            },
            StatusCode::Forbidden => {
                Ok(Response::Forbidden)
            },
            _ => {
                if status.is_server_error() {
                    Ok(Response::InternalServerError)
                } else {
                    Err(request::Error::UnexpectedApiResponse)
                }
            }
        }
    }
}

impl<'a> Requestable for TriggerEvent<'a> {
    type Response = Response;

    fn body(&self) -> String {
        to_string(&self).unwrap()
    }

    fn method(&self) -> Method {
        Method::Post
    }

    fn get_response(status: StatusCode,
                    headers: &Headers,
                    body: &str) -> request::Result<Response> {
        Response::get_response(status, headers, body)
    }
}


/// Send a TriggerEvent request
pub fn trigger(auth: &AuthToken, event: &TriggerEvent) -> request::Result<Response> {
    request::perform(auth, event)
}

/// Send a ResolveEvent request
pub fn resolve(auth: &AuthToken, event: &ResolveEvent) -> request::Result<Response> {
    request::perform(auth, event)
}

/// Send an AcknowledgeEvent request
pub fn acknowledge(auth: &AuthToken, event: &AcknowledgeEvent) -> request::Result<Response> {
    request::perform(auth, event)
}

#[cfg(test)]
mod tests {
    use super::{TriggerEvent, Context};

    use serde_json::{from_str, to_string, Value as Json};

    #[test]
    fn context_to_json() {
        let expected: Json = from_str(stringify!({
            "type": "image",
            "src": "https://www.example.com"
        })).expect("expected is valid json");

        let context = Context::image("https://www.example.com", None, None);
        let json_string = to_string(&context).unwrap();
        let actual: Json = from_str(&json_string).unwrap();

        assert_eq!(actual, expected);
    }

    #[test]
    fn trigger_event_to_json() {
        let expected: Json = from_str(stringify!({
            "event_type": "trigger",
            "service_key": "the service key",
            "description": "Houston, we have a problem"
        })).expect("expected is valid json");

        let event = TriggerEvent::new("the service key", "Houston, we have a problem");
        let json_string = to_string(&event).unwrap();
        let actual: Json = from_str(&json_string).unwrap();

        assert_eq!(actual, expected);
    }


    #[test]
    fn trigger_event_with_contexts_to_json() {
        #[derive(Debug, Serialize)]
        struct Details {
            last_delivery_time: i32,
        }

        let expected: Json = from_str(stringify!({
            "event_type": "trigger",
            "service_key": "the service key",
            "description": "Houston, we have a problem",
            "contexts": [
                {
                    "type": "image",
                    "src": "https://www.example.com"
                },
                {
                    "type": "link",
                    "href": "https://www.example.com",
                    "text": "a link"
                }
            ],
            "details": {
                "last_delivery_time": 10
            },
            "incident_key": "KEY123"
        })).expect("expected is valid json");

        let event = TriggerEvent::new("the service key", "Houston, we have a problem")
                        .set_incident_key("KEY123")
                        .set_details(&Details { last_delivery_time: 10 })
                        .add_context(Context::image("https://www.example.com", None, None))
                        .add_context(Context::link("https://www.example.com", "a link"));

        let json_string = to_string(&event).unwrap();
        let actual: Json = from_str(&json_string).unwrap();

        println!("{:?}", event);

        assert_eq!(actual, expected);
    }
}

#[cfg(feature = "live_tests")]
mod live_tests {
    use AuthToken;

    use super::{trigger, Response, TriggerEvent};

    #[test]
    fn invalid_auth_token_is_rejected() {
        let event = TriggerEvent::new("0123456789abcdef0123456789abcdef", "Test event");
        let token = AuthToken::new("abc");
        let response = trigger(&token, &event).unwrap();

        match response {
            Response::Success(_) => (),
            _ => panic!("Should have been success")
        }
    }
}