こんにちは!
ポーラ・オルビスホールディングスのITプロダクト開発チームでスクラムマスターをしている川田です。
引き続き、Four Keysの各指標についてNewRelicを利用して可視化することを目指します。前回までの記事はこちらです。
変更障害率とは
今回は、Four Keysで定義された4つの指標のうち「変更障害率」について可視化を行います。前回と同様、最初に変更障害率のパフォーマンスレベルの定義を確認します。
Elite | High | Medium | Low |
---|---|---|---|
0% - 15% | 16% - 30% | 16% - 30% | 16% - 30% |
DORAの定義ではHigh/Medium/Lowの定義が同じであるため、今回はシンプルに15%以下ならElite、16%以上ならHighとする判定ロジックにします。
変更障害率の定義は、Google Cloudのブログには以下のように記載されています。
For the primary application or service you work on, what percentage of changes of production or released to users result in degraded service (e.g., lead to service impairment or service outage) and subsequently require remediation (e.g., require a hotfix, rollback, fix forward, patch)?
あなたが携わっている主要なアプリケーションまたはサービスにおいて、本番環境への変更またはユーザーへのリリースがサービス品質の低下(例:サービス障害やサービス停止)を引き起こし、その後に対応措置(例:ホットフィックス、ロールバック、フィックスフォワード、パッチの適用)が必要となる割合はどの程度ですか?
この内容をもとに考えると
- 本番環境にリリースしたバージョン
- 当該バージョンでサービス品質が低下したかどうかの情報
の2つがわかれば計算できそうですが、サービス品質が低下したことの判断方法は考慮が必要です。サービスが停止したり、エラーが発生したりするような障害であればログ等から容易に判断できますが、サービスによってはそのような機械的な判断が難しいケースも存在すると思います。よって、自分たちのサービスに対する「サービス品質の低下の定義」を定め、それをどの情報から収集するか設計することが重要です。
私たちのチームで過去に発生した問題を振り返ったところ、一般的なシステム障害はほとんど無く、ユーザーの作業や操作が止まってしまうケースであることがわかりました。そのため、それらの問題に対して発生確率や影響度の指標を定め、一定の基準を超えたもの=サービス品質が低下した、と定義することにしました。基準の判断はどうしても人手で行う必要があるため、判断結果をBacklog(プロジェクト管理ツール)に入力し、入力された内容をNewRelicに連携する形を検討します。
前回と同様に本記事ではDORAの定義をそのまま利用しますが、実際のチームに適用する際にはチームの状況にあった定義をあらためて検討しましょう。
Backlogに入力された内容をNewRelicに連携する
弊社ではプロジェクト管理ツールとしてBacklogを使用しているため、まずはその内容をNewRelicに連携する部分を実装します。Backlogから情報を取得するには、
- Backlog APIを利用してチケットの情報を定期的に取得する
- チケットに変更が発生したら通知されるWebhookを利用する
のどちらかで実現が可能です。今回はWebhookを利用し、Amazon API Gatewayを経由して内容をAWS Lambdaに連携した後、Lambda上で内容のフィルタリングや整形を行ってNewRelicに連携する方針で進めます。NewRelicへの送信は前回までと同様にカスタムイベントを利用するので、あらかじめデータ送信先のNewRelicアカウントのアカウントIDとデータ送信のためのライセンスキーを払い出しておいてください。
まずはAPI GatewayとLambdaのセットアップを行いますが、今回の本題ではないので詳細は省略します。公式ドキュメント等を参照してセットアップし、API Gatewayのステージ画面から対象のAPIのURLをコピーしておいてください。
続いてBacklog側の設定を行います。公式ドキュメントを参考に、先ほどコピーしたURLをWebhook URLとして設定します。通知するイベントについては、今回は「課題の追加」と「課題の更新」だけにチェックを入れます。
あわせて、バグの分類と混入バージョンの2つを入力できるようにカスタム属性として追加しておきます。デフォルトで用意されているフィールドを利用する場合は、適宜読み替えてください。
ここまで設定するとBacklogでチケットを追加・更新した際にWebhookで通知が飛ぶようになるため、あとはLambdaの実装を行います。実装内容についてパートごとに見ていきます。
まずは必要な値の取得部分です。
import json
import os
import urllib.request
from datetime import datetime
def lambda_handler(event, context):
# 環境変数から値を取得
newrelic_account_id = os.getenv("NEWRELIC_ACCOUNT_ID")
newrelic_api_key = os.getenv("NEWRELIC_API_KEY")
backlog_bug_ticket_id = int(os.getenv("BACKLOG_BUG_TICKET_ID"))
backlog_custom_bug_kind_id = int(os.getenv("BACKLOG_CUSTOM_BUG_KIND_ID"))
backlog_custom_bug_version_id = int(os.getenv("BACKLOG_CUSTOM_BUG_VERSION_ID"))
# 入力JSONの解析
project = event.get("project", {})
operation_type = event.get("type")
project_key = project.get("projectKey")
content = event.get("content", {})
key_id = content.get("key_id")
issueType = content.get("issueType", {})
status = content.get("status", {})
今回は定数値をLambdaの環境変数に登録したので、そちらから値を取得します。以降の処理でBacklogのチケット種別のID、追加したカスタム属性のIDが必要となるため、それらもあらかじめ環境変数に登録しています。
なお種別のIDはBacklogのプロジェクト設定から対象の種別を選択した際に表示されるURLの issueType.id
に指定されている値です。
https://your-space.backlog.jp/EditIssueType.action?issueType.id=<ここの値>&issueType.projectId=0000000000
同様に、カスタム属性のIDは対象のカスタム属性を選択した際に attribute.id
に指定されている値です。
https://your-space.backlog.jp/EditAttribute.action?attribute.id=<ここの値>
続いて、追加のデータ取得やフィルタリング処理を行う部分です。
# バグチケットではない場合、処理を中断
if issueType["id"] != backlog_bug_ticket_id:
print(f"Not a bug ticket : {key_id}")
return {"statusCode": 200, "body": "Not a bug ticket"}
# バグ分類/バグ混入バージョンの値を取得
customFields = content.get("customFields", [])
bug_kind = next(
(c["value"] for c in customFields if c["id"] == backlog_custom_bug_kind_id), ""
)
bug_version = next(
(c["value"] for c in customFields if c["id"] == backlog_custom_bug_version_id),
"",
)
operated_at = int(datetime.fromisoformat(event.get("created")).timestamp())
closed_at = None
if status["id"] == 4:
# チケットのステータスが完了(status = 4)なら完了時間として送信
closed_at = operated_at
Backlogから渡されるJSONの content.issueType.id
にチケットの種別が格納されているので、この値がバグチケットの種別かどうか確認します。この処理を行わずにNewRelicでフィルタリングすることも可能ですが、NewRelicに記録するデータ量の削減を目的として不要なデータは送らないようにしています。
続けて content.customFields
からカスタム属性の値を取得し、最後にチケットが操作された時間(created
)を取得します。操作された時間はチケットのステータスによって詰め直していますが、これはNewRelic側のクエリを単純化するための実装です。1つの時刻のフィールドを別の意味で扱うことでクエリが複雑になるのであれば、可読性や保守性を優先して、多少冗長な構造になることは許容しました。
最後に、NewRelicにデータを送信する部分です。
# 送信するJSONデータの作成
payload = {
"eventType": "Bug",
"project_key": project_key,
"key_id": key_id,
"operation_type": operation_type,
"status": status["id"],
"bug_kind": bug_kind["id"] if bug_kind else None,
"bug_version": bug_version,
"operated_at": operated_at,
"closed_at": closed_at,
}
# APIエンドポイントの作成
url = f"https://insights-collector.newrelic.com/v1/accounts/{newrelic_account_id}/events"
# APIリクエストのヘッダー
headers = {"Content-Type": "application/json", "X-Insert-Key": newrelic_api_key}
# NewRelicにデータを送信
res = urllib.request.Request(
url, data=json.dumps(payload).encode("utf-8"), headers=headers, method="POST"
)
try:
with urllib.request.urlopen(res) as response:
response_body = response.read().decode("utf-8")
status_code = response.getcode()
except urllib.error.HTTPError as e:
response_body = e.read().decode("utf-8")
status_code = e.code
return {"statusCode": status_code, "body": response_body}
eventType
に指定するカスタムイベントの名称は Bug
としています。今まで集めたデータをペイロードに詰めて、NewRelicのカスタムイベントAPIをコールするだけです。
NewRelicでパフォーマンスレベルを判定し可視化する
必要なデータがNewRelicに集まったので、いよいよパフォーマンスレベルを判定した結果を可視化する部分を実装します。
まずはいつも通り、NewRelicのクエリビルダーでデータを確認してみます。この記事を書いているタイミングでは幸いなことにあまり不具合が出ていなかったので、直近3カ月の情報を取得します。
SELECT *
FROM Bug
SINCE 3 months ago
結果は以下の通り、ちゃんと記録されています!
Webhookによる連携の場合はチケットに変更が発生する度にデータが連携されるため、同じチケット番号(key_id)のレコードが複数記録されてしまいます。そのため、FACET を利用してチケット番号でグルーピングした後、latest関数を用いて最終的なチケットの内容を取得するようにします。この後の処理で必要な、バグ分類(bug_kind)とバグの混入バージョン(bug_version)で試します。
SELECT
latest(bug_kind) AS kind,
latest(bug_version) AS version
FROM Bug
FACET key_id
SINCE 3 months ago
以下のように、key_idでグルーピングした最新のデータのみが取得できました!
続いて、上記の情報を以前のデプロイ頻度の可視化時に記録したデプロイイベントと結合し、当該のバージョンでバグが混入したと記録されたチケットの数をカウントしてみます。データの結合には JOIN を使います。
Backlogのカスタム属性で、bug_kind=1
は重大なインシデント(サービス品質が低下した対象の不具合)、2
は軽微な不具合としているので、それぞれ分けてカウントします。filter関数によって条件に合うデータだけを簡単に集計できるので、こちらを使います。また、バグが発生していないバージョンのカウントはNULLになり結果が空欄になるため、明示的に0件であることをわかりやすくするように if関数 で0にします。
SELECT
version,
if(incident_count IS NULL, 0, incident_count) AS 'incident_count',
if(bug_count IS NULL, 0, bug_count) AS 'bug_count'
FROM (
SELECT
latest(product) AS product
FROM Deployment
WHERE environment = 'prd'
FACET version
) LEFT JOIN (
SELECT
filter(count(*), WHERE bug_kind = 1) AS incident_count,
filter(count(*), WHERE bug_kind = 2) AS bug_count
FROM (
SELECT
latest(bug_kind) AS bug_kind,
latest(bug_version) AS version,
latest(bug_kind) AS bug_kind
FROM Bug
FACET key_id
)
FACET version
) ON version
ORDER BY version ASC
SINCE 3 months ago
結果は以下のようになりました。ばっちりカウントできています!
あとはレベルに応じた基準と比較する部分を実装して完成です。前述までのクエリと異なる点として、bug_kind=1
のインシデントのみを対象とするロジックを入れています。最終的には以下のようなクエリとなりました。
SELECT
if(count(*) = 0 OR filter(count(*), WHERE incident_count > 0) / count(version) <= 0.15,
'Elite', 'High') AS ''
FROM (
SELECT version
FROM Deployment
WHERE environment = 'prd'
) LEFT JOIN (
SELECT count(key_id) AS incident_count
FROM (
SELECT
latest(bug_version) AS version,
latest(bug_kind) AS bug_kind
FROM Bug
FACET key_id
)
WHERE bug_kind = 1 // インシデントのみ対象
FACET version
) ON version
SINCE 3 months ago
こちらを実行すると、無事レベルが表示されました!
さいごに
Four Keysの可視化の3回目として、今回は変更障害率をNewRelic上で可視化してみました。参考になれば幸いです。
次回は最後、サービス復元時間の可視化にトライします!