エラー発生時に自動的にJIRAチケットを起票したい
エラーが発生した際、気づける仕組みや対応の未済管理ができる仕組みは必要です。
これらがないとエラーが発生していることに気づかず、長期にわたってユーザーに迷惑をかけてしまう恐れがあります。
業務でシステム運用している際は、自動的にエラーを検知し、PagerDutyをはじめとしたインシデント管理製品に連携するのが一般的かと思います。
個人開発でもライトにエラー検知と未済管理をしたいと思い、エラー発生時にJIRAチケットを自動起票する仕組みを試してみました。
システム構成
以下のような流れです。
省略していますが、JIRAチケットを起票するために必要なメールアドレスやアクセストークンはSecretManagerで管理しています。
やってみた
検証用のアプリケーション
SpringBoot + Spring Security + Thymeleafでログインするだけのアプリを用意しました。
Spring Initializrで作ったひな型プロジェクトをベースにAntigravity CLIで作ってもらいました。
このアプリですが、なんと 「ユーザー名に数字以外が含まれていたらログイン時にシステムエラーになる」 という致命的なバグがあります。
(検証のためにわざと埋め込みました)
埋め込んだバグは以下の個所です。
@Controller
public class LoginController {
// 割愛
@GetMapping("/")
public String home(Model model, Authentication authentication) {
if (authentication != null) {
model.addAttribute(
"username",
Integer.parseInt(authentication.getName()) // ★検証用に仕込んだバグ!ユーザー名に数字以外が含まれていたらエラー
);
}
return "index";
}
}
アラートポリシーと通知チャンネル
Terraformで作りました。
# Pub/Sub 通知チャンネル
resource "google_monitoring_notification_channel" "pubsub" {
display_name = "Cloud Run Error Log Pub/Sub Notification Channel"
project = var.project_id
type = "pubsub"
labels = {
topic = google_pubsub_topic.cloud_run_error_logs.id
}
depends_on = [google_project_service.required]
}
# Cloud Runのエラーログを検知するアラートポリシー
resource "google_monitoring_alert_policy" "cloud_run_error_alert" {
display_name = "Cloud Run Error Logs Alert Policy"
project = var.project_id
combiner = "OR"
conditions {
display_name = "Cloud Run Error Log Detected"
condition_matched_log {
filter = <<-EOT
resource.type="cloud_run_revision"
severity>=${var.error_log_severity}
resource.labels.service_name!="${var.jira_ticket_creator_service_name}"
logName="projects/${var.project_id}/logs/run.googleapis.com%2Fstdout"
EOT
label_extractors = {
"service_name" = "EXTRACT(resource.labels.service_name)"
"revision_name" = "EXTRACT(resource.labels.revision_name)"
"log_message" = "EXTRACT(jsonPayload.message)"
"stack_trace" = "EXTRACT(jsonPayload.stack_trace)"
}
}
}
notification_channels = [
google_monitoring_notification_channel.pubsub.name
]
alert_strategy {
notification_rate_limit {
period = "300s"
}
auto_close = "1800s"
}
# Markdown形式でドキュメントを記述(log.extracted_label.[KEY]変数を使用)
# ドキュメントをJIRAチケットのsummaryとdescriptionとして利用する
documentation {
mime_type = "text/markdown"
content = <<-EOT
# [${var.error_log_severity}] Cloud Run: $${log.extracted_label.service_name} で $${log.extracted_label.log_message} が発生しました
Cloud Runサービス「$${log.extracted_label.service_name}」でエラーログを検知しました。
### ログ詳細メッセージ
```
$${log.extracted_label.stack_trace}
```
### インシデント詳細
- **サービス名**: $${log.extracted_label.service_name}
- **リビジョン名**: $${log.extracted_label.revision_name}
- **ポリシー名**: $${policy.display_name}
EOT
}
depends_on = [google_project_service.required]
}
condition_matched_logで指定しているログのフィルタ条件は、下記2つが重要です。
resource.labels.service_name!="${var.jira_ticket_creator_service_name}"が前述の無限エラー地獄を避ける除外条件です。
logName="projects/${var.project_id}/logs/run.googleapis.com%2Fstdout"は、SpringBootアプリケーションが出力したエラーログだけを検知するための条件です。
500応答を返すとCloud Runのリクエストログもseverity = ERRORでログ出力するため、1つの事象で複数チケット起票を避けるために入れています。
label_extractorsはアラートポリシーによってインシデントが作成されたとき、説明文にログの内容を埋め込むための仕組みです。
ログ中の項目を変数に割り当て、documentationブロックで定義したMarkdown形式の説明文中で利用します。
ログから抽出した項目は$${log.extracted_label.KEY名}という形式で変数展開できます。
その他、documentationで利用できる変数は以下のドキュメントを参照ください。
JIRAチケット起票をするアプリケーション
express(TypeScript)で開発しました。
監視対象アプリはSpringBoot(Java)で用意しましたが、勉強のためにフレームワークと言語を変えてみました。
Pub/SubのメッセージをCloud Runで読み取る方法は以下のドキュメントを参照ください。
Pub/Subのメッセージを元にJIRAチケットを起票する処理は以下の通りです。
function buildDescription(descriptionText: string) {
// Atlassian Document Format (ADF)
const lines = descriptionText.split('\n');
const paragraphs = lines.map((line) => ({
type: 'paragraph',
content: line ? [{ type: 'text', text: line }] : [],
}));
return {
type: 'doc',
version: 1,
content: paragraphs.length > 0 ? paragraphs : [{ type: 'paragraph', content: [] }],
};
}
export async function createIssue(config: Config, elements: JiraIssueElements): Promise<string> {
const url = `${config.jiraBaseUrl}/rest/api/3/issue`;
const payload = {
fields: {
project: { key: config.jiraProjectKey },
issuetype: { name: config.jiraIssueType },
summary: truncate(elements.summary, 250),
labels: [`incident-${elements.incidentId}`, 'auto-created', 'cloud-run-error'],
description: buildDescription(elements.description),
},
};
const res = await fetch(url, {
method: 'POST',
headers: {
Authorization: authHeader(config),
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(payload),
});
if (!res.ok) {
throw new Error(`JIRAチケット作成に失敗しました: ${res.status} ${await res.text()}`);
}
const body = (await res.json()) as JiraCreateIssueResponse;
return body.key;
}
elements: JiraIssueElementsはpub/subメッセージから必要な要素を抽出したオブジェクトです。
アラートポリシーで作成したドキュメントはincident.documentation.contentでアクセスできます。
今回は、ドキュメント先頭の1文をsummaryとし、残りをdescriptionとして利用しました。
エラーを発生させてみた結果
Google Cloudコンソール上の表示
アラートポリシーに合致するログが検出されるとインシデントが作成されます。
お試しアプリでシステムエラーを発生させたところ以下のように表示されました。
ドキュメントの表示範囲が狭い点は難点ですね…
Cloud Monitoring上でインシデントのステータス管理ができるので、単純な未済管理だけで良ければJIRA等のチケット管理システムを使わない選択肢も取れます。
JIRA
無事チケット起票されていました!
~中略~
まとめ
アラートポリシーはインシデントがオープンな間は複数回のエラーログをオープン中のインシデントに集約してくれる点が便利ですね。
実運用を考えた時、大量のアラートが発生すると重要なアラートを見逃しかねないので、不要にアラートが増えることを避ける仕組みは重要だと思います。
一方、アラートポリシーではnotification_rate_limitで指定した時間が経過しないとアラート作成されないので、アラート検出までのリードタイムが長くなる点はデメリットです。
このデメリットを避ける場合はアラートポリシーを使わない選択肢もありかもしれません。









