PagerDutyのAPIの使い方

この記事は[PagerDuty Advent Calendar 2023](https://qiita.com/advent-calendar/2023/pagerduty)の23日目の記事です。

---

どうも、[manji0](https://x.com/_manji0)です。 今年は[所属してる会社が合併したり](https://www.lycorp.co.jp/ja/news/release/000846/)、交際0日婚約 & 出会って2ヶ月で入籍したりと中々激動の一年でした。

今日はPagerDutyのAPIの使い方について、いくつか例を上げたいと思います。PDのAPIを使いこなすための導入記事として読んでいただければ嬉しいです。

## PagerDuty API

まずはお手元に[PagerDuty APIのドキュメント](https://developer.pagerduty.com/api-reference/e65c5833eeb07-pager-duty-api)を用意しておいてください。

Service, Escalation PolicyやScheduleなど、かなり広い対象にAPIが用意されていることが見てとれると思います。ただ、ほとんどのユーザはEvent API v2(=Incidentを作成するためのAPI)以外のAPIを使ったことがないのではないでしょうか?

個人的には、PDのAPIは少し使い方に癖があるように思えます。やりたいことを実現する方法をすぐに思いつけるようになるのに多少の慣れが必要だな、という感覚です。

例えば、[Get a schedule](https://developer.pagerduty.com/api-reference/3f03afb2c84a4-get-a-schedule)をQuery param無しでコールしてみます。

ドキュメントのResponse sampleでは以下のように `final_schedule` が返ってきますね。

```json
{
  "schedule": {
    "id": "PI7DH85",
    "type": "schedule",
    "省略": "",
    "final_schedule": {
      "name": "Final Schedule",
      "rendered_schedule_entries": [
        {
          "start": "2015-11-10T08:00:00-05:00",
          "end": "2015-11-10T17:00:00-05:00",
          "user": {
            "id": "PXPGF42",
            "type": "user_reference",
            "summary": "Regina Phalange",
            "self": "https://api.pagerduty.com/users/PXPGF42",
            "html_url": "https://subdomain.pagerduty.com/users/PXPGF42"
          }
        }
      ],
      "rendered_coverage_percentage": 37.5
    }
  }
}
```

ところが実際に叩いてみると、final_scheduleは空だったりします。

```json
    "overrides_subschedule": {
      "name": "Overrides",
      "rendered_schedule_entries": [],
      "rendered_coverage_percentage": null
    },
    "final_schedule": {
      "name": "Final Schedule",
      "rendered_schedule_entries": [],
      "rendered_coverage_percentage": null
    },
```

これが何故か分かる方は、正直この記事を読まなくてもOKです。

そうでない方がPDのAPIを便利に使えるようになるために、「あると少し便利だな」というユースケースを例に挙げ、仕様を交えながらAPIの呼び出しフローを説明していきます。

## ユースケース 

### 任意のEscalation Policyから現在の担当者のリストを取得する

**用途: Slackにその週の担当者を通知する、など**

**まとめ:**

1. Get an escalation policyを使って`escalation_rules.0.targets`を取得
1. targetsにtype:schedule_referenceが含まれる場合はGet a scheduleを使って`final_schedule`を取得し、担当者を特定する

---

目的に沿ってそうな名前のAPIは[Get an escalation policy](https://developer.pagerduty.com/api-reference/3db5a206585e1-get-an-escalation-policy)ですね。response exampleを見てみましょう。

```json
{
  "escalation_policy": {
    "id": "PT20YPA",
    "type": "escalation_policy",
    "summary": "Another Escalation Policy",
    "on_call_handoff_notifications": "if_has_services",
    "self": "https://api.pagerduty.com/escalation_policies/PT20YPA",
    "html_url": "https://subdomain.pagerduty.com/escalation_policies/PT20YPA",
    "name": "Another Escalation Policy",
    "escalation_rules": [
      {
        "id": "PGHDV41",
        "escalation_delay_in_minutes": 30,
        "targets": [
          {
            "id": "PAM4FGS",
            "summary": "Kyler Kuhn",
            "type": "user_reference",
            "self": "https://api.pagerduty.com/users/PAM4FGS",
            "html_url": "https://subdomain.pagerduty.com/users/PAM4FGS"
          },
          {
            "id": "PI7DH85",
            "summary": "Daily Engineering Rotation",
            "type": "schedule_reference",
            "self": "https://api.pagerduty.com/schedules/PI7DH85",
            "html_url": "https://subdomain.pagerduty.com/schedules/PI7DH85"
          }
        ]
      }
    ],
    "services": [
      {
        "id": "PIJ90N7",
        "type": "service_reference",
        "summary": "My Mail Service",
        "self": "https://api.pagerduty.com/services/PIJ90N7",
        "html_url": "https://subdomain.pagerduty.com/service-directory/PIJ90N7"
      }
    ],
    "num_loops": 2,
    "teams": [
      {
        "id": "PQ9K7I8",
        "type": "team_reference",
        "summary": "Engineering",
        "self": "https://api.pagerduty.com/teams/PQ9K7I8",
        "html_url": "https://subdomain.pagerduty.com/teams/PQ9K7I8"
      }
    ],
    "description": "This is yet another escalation policy"
  }
}
```

`escalation_rules` が目的の情報のようですが、このままでは使えませんね。Kylerさんが担当者なのは判明していますが...

```json
          {
            "id": "PI7DH85",
            "summary": "Daily Engineering Rotation",
            "type": "schedule_reference",
            "self": "https://api.pagerduty.com/schedules/PI7DH85",
            "html_url": "https://subdomain.pagerduty.com/schedules/PI7DH85"
          }
```


このAPIから得られる情報では、sheduleの担当者は明らかになっていません。

**Scheduleで定義された担当者を含めた完全なリストを生成するには、Scheduleの情報も取得する必要があります。**

#### Get a scheduleについて

[Get a schedule](https://developer.pagerduty.com/api-reference/3f03afb2c84a4-get-a-schedule)を使ってみましょう。担当者は`final_schedule`に記載されるはずです。

...しかし、(恐らく)ほとんどの方はここが空になって返ってくるのではないでしょうか。僕の試した限り、**`final_schedule`を得るためには`since`か`until`を指定する必要があります**。

そして、ドキュメントにある通りsinceかuntilの片方だけを指定した場合には**そこを起点とした2週間分の情報が出力されます。**

例えば、1週間で担当者が切り替わるスケジュールである場合は最大3週分の情報が出力されます。

```
curl -H "Authorization: Token token=xxx" https://api.pagerduty.com/schedules/yyy\?since\=2023-12-23T00:00:00 | jq
```

```json
# 省略
"final_schedule": {
      "name": "Final Schedule",
      "rendered_schedule_entries": [
        {
          "start": "2023-12-23T00:00:00+09:00",
          "end": "2023-12-29T20:00:00+09:00",
          "user": {
            "id": "YYY",
            "type": "user_reference",
            "summary": "Alice Carrol",
            "self": "https://api.pagerduty.com/users/YYY",
            "html_url": "https://verda.pagerduty.com/users/YYY"
          },
          "id": "Q114LVW901RIEH"
        },
        {
          "start": "2023-12-29T20:00:00+09:00",
          "end": "2024-01-05T20:00:00+09:00",
          "user": {
            "id": "XXX",
            "type": "user_reference",
            "summary": "Bob Iris",
            "self": "https://api.pagerduty.com/users/XXX",
            "html_url": "https://verda.pagerduty.com/users/XXX"
          },
          "id": "Q0BY6F774SDSHE"
        },
        {
          "start": "2024-01-05T20:00:00+09:00",
          "end": "2024-01-06T00:00:00+09:00",
          "user": {
            "id": "ZZZ",
            "type": "user_reference",
            "summary": "Adam Smith",
            "self": "https://api.pagerduty.com/users/ZZZ",
            "html_url": "https://verda.pagerduty.com/users/ZZZ"
          },
          "id": "HOGEHOGE"
        }
      ],
      "rendered_coverage_percentage": 100.0
    }
```

こうして何個も出力されてしまったら、「現在の日付とrendered_schedule_entriesのそれぞれの要素の.startや.endを比較して適切なユーザを抽出する」みたいなとても面倒な手順が必要になってしまいます。

これを避けるには、`since`と`until`に同じように現在のtimestampを指定してください。

```
curl -H "Authorization: Token token=xxx" https://api.pagerduty.com/schedules/yyy\?since\=2023-12-23T00:00:00\&until\=2023-12-23T00:00:00 | jq
```

```json
    "final_schedule": {
      "name": "Final Schedule",
      "rendered_schedule_entries": [
        {
          "start": "2023-12-22T20:00:00+09:00",
          "end": "2023-12-29T20:00:00+09:00",
          "user": {
            "id": "YYY",
            "type": "user_reference",
            "summary": "Alice Carrol",
            "self": "https://api.pagerduty.com/users/YYY",
            "html_url": "https://verda.pagerduty.com/users/YYY"
          },
          "id": "HOGEHOGE"
        }
      ],
      "rendered_coverage_percentage": 100.0
    }
```

これで担当者が全員明らかになりました。使い方が少し分かってきましたでしょうか。

### 一定期間内に発生したアラートを全て取得する

**用途： インシデントをローカルのpandasなどでより詳細に分析するなど**

**まとめ:**

1. List incidentsから指定期間内のIncident一覧を取得
1. Incident毎にList alerts for an incidentでAlert一覧を取得してマージ
1. (**`resolved_at`はAlertではなくIncidentにしか定義されてないので注意**)

---

PagerDutyではIncidentとAlertは別の概念です。具体的には、1つのIncidentには複数のAlertが含まれる場合があります。

通常は1Incident=1Alertですが、上位プランで[Incident Alert Grouping](https://support.pagerduty.com/lang-ja/docs/intelligent-alert-grouping)を使っていたり、PDのEvent API v2を叩くモジュール(Alertmanagerなど)でdedup keyを制御したりしていると、この関係が崩れることがあります。

(ちなみに、Event API v2でPOSTしたときに作成されるのは(僕の認識が正しければ)Alertです。Incidentが作成されるか、既存のIncidentにマージされるかはPOST後に判断されます。)

ということで、手元のシステムから発行されるアラートなどについて件数などを正確に分析したい場合は、**すべてのIncidentからAlertの一覧を抽出する必要があります**。

[List incidents](https://developer.pagerduty.com/api-reference/9d0b4b12e36f9-list-incidents)から仕様を確認していきましょう。

* Query parameterにはGet a scheduleと同じく`since`と`until`があるので、期間はここで指定します。
* 対象のサービスを絞りたい場合には、`services`にサービスIDのリストを指定します。チーム管理をきっちりしてる場合には`teams`で絞ってもよいです。
* `more:true`の場合には`offset`を使って追加でincident listを取得しましょう。

Response Exampleを見る限り、`incidents[].id`以外の情報は無視していいですね。

次に、[List alerts for an incident](https://developer.pagerduty.com/api-reference/4bc42e7ac0c59-list-alerts-for-an-incident)を使ってAlertのリストを取得します。最後にそれをマージしていけばAlertのリストの完成です。

さて、List alerts for an incidentや[Get an alert](https://developer.pagerduty.com/api-reference/592e890ae8c99-get-an-alert)のResponse Exampleを見てみると、`created_at`はありますが`resolved_at`がないことが分かります。

実は、PagerDutyでは`resolved_at`はIncidentにしか定義されていません。まあIncidentがオンコールイベントなので自然ですね。

とはいえ、このように「PDに投げたアラートをすべて管理したい」とか「PDのAnalysis機能以上の分析をしたい」という場合には引っかかりやすい仕様だと思います。このあたり、自分たちの要望は発生次第しっかり担当の営業さんに伝えて実現方法を一緒に考えてもらいましょうね。

---

結構長くなったのでこの辺で。直前に書いたことにも繋がりますが、一般論として、APIやドキュメントが整備されてるからといって自分たちで何でも考えようとすると後から詰まったりするものです。PagerDutyは契約してたらきちんと担当営業さんがついてくれるので、逐一相談してより快適なオンコール生活を送りましょう。 Merry Xmas and Have a happy new year!

---

余談: 実はIncidentのリストはWebUIからならCSVで落とせたりします。

![スクリーンショット 2023-12-23 0.05.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920938/8325cba1-ae41-9778-eab3-9344cf9fa1b2.png)

分析のためにPandasにかけるならCSVで落とした方が圧倒的に楽なので、このAPIを開放してください! クリスマスプレゼントってことで!