0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Lambda ログ監視を Backlog に連携する ― 方式①②の動作検証と比較

0
Posted at

はじめに

前回の記事では、Lambda のエラーログ監視として以下の2方式を CloudFormation テンプレートで実装・比較しました。

方式 構成 特徴
① Metric Filter + CloudWatch Alarm + SNS ログを1分ごとに集計 → 閾値超過でメール シンプル・Lambda 不要・約2分の遅延
② Subscription Filter + 通知 Lambda ログをリアルタイム転送 → API 起票 リッチ・ログ内容あり・数秒で通知

今回は 実際に Backlog へ連携し、両方式の動作を検証します。通知のタイミング・Backlog 課題の内容・運用上のハマりポイントを実測値と合わせて紹介します。


前提:運用フロー

前回記事と同じく、24/365 オペレータ常駐を想定した運用フローです。

Lambda でエラーが発生
      ↓
Backlog に課題が自動登録
      ↓
24/365 オペレータが Backlog を監視・確認
      ↓
優先度に応じて開発者に連絡
      ├── 高優先度 → 即時コール
      └── 低優先度 → 翌営業日対応
      ↓
開発者がインシデント対応開始

検証環境

監視対象 Lambda(共通)

両方式で同じ Lambda 関数をエラー発生源として使います。

import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    logger.info("Starting process...")
    logger.error("Database connection failed: timeout after 30s")
    logger.error("NullPointerException at module.process_data line 42")
    logger.info("Process ended with errors")
    raise Exception("Application error: critical failure")

実行すると CloudWatch Logs に以下のログが記録されます。

[INFO]  2026-06-27T09:54:00Z  xxxx  Starting process...
[ERROR] 2026-06-27T09:54:00Z  xxxx  Database connection failed: timeout after 30s
[ERROR] 2026-06-27T09:54:00Z  xxxx  NullPointerException at module.process_data line 42
[INFO]  2026-06-27T09:54:00Z  xxxx  Process ended with errors
[ERROR] Exception: Application error: critical failure

方式①:Metric Filter + CloudWatch Alarm → SNS → Backlog メール

構成図

監視対象 Lambda
    ↓ ログ出力
CloudWatch Logs
    ↓ Metric Filter("[ERROR]" を含む行をカウント)
カスタムメトリクス(LambdaErrorCount)
    ↓ CloudWatch Alarm(1分間に1件以上で ALARM)
SNS Topic
    ├─→ Backlog 課題登録用メールアドレス → 課題自動登録
    └─→ 担当者メール(確認用)

設定手順

⚠️ Metric Filter パターンのハマりポイント

Lambda のログ形式は \t[ERROR]\t{timestamp}\t{requestId}\t{message} です。
構造化パターン [timestamp, requestId, level="ERROR", ...] ではマッチしません。

# ❌ マッチしない(今回の検証で実際にハマった)
--filter-pattern '[timestamp, requestId, level="ERROR", ...]'

# ✅ 正しい(キーワード検索を使う)
--filter-pattern '"[ERROR]"'

CloudFormation テンプレート

前回記事のテンプレートから変更点は SNS のサブスクリプション先を Backlog のメールアドレスにする点のみです。

AlertSNSEmailSubscription:
  Type: AWS::SNS::Subscription
  Properties:
    TopicArn: !Ref AlertSNSTopic
    Protocol: email
    Endpoint: issue-{PROJECT_KEY}-{TOKEN}@{SPACE}.backlog.com  # ← Backlog の課題登録用メールアドレス

⚠️ SNS サブスクリプション確認メールへの対応

SNS にメールアドレスをサブスクライブすると確認メールが送信されます。
Backlog の課題登録用アドレスはどんなメールでも課題として登録するため、確認メール自体が課題として登録されます

サブスクリプションを有効にするには、Backlog に登録された確認課題の本文中にある 「Confirm subscription」 リンクをクリックしてください。

動作検証結果

Lambda を3回実行後、約2分で CloudWatch Alarm が ALARM 状態に遷移しました。

Lambda 実行:        2026-06-27 18:54:56 JST
Alarm 発火:         2026-06-27 18:55:54 JST(約60秒後)
Backlog 課題登録:   2026-06-27 18:56:xx JST(さらに数十秒後)

Alarm の根拠:
  1 datapoint [9.0] was greater than or equal to the threshold (1.0)
  ※ 3回の Lambda 実行 × 各3件の ERROR ログ = 9 件カウント

SNS から届くメール文面(= Backlog 課題内容)

方式①では SNS が生成するアラーム通知メールがそのまま Backlog の課題詳細になります。

件名: ALARM: "monitored-app-error-alarm" in Asia Pacific (Tokyo)

You are receiving this email because your Amazon CloudWatch Alarm
"monitored-app-error-alarm" in the Asia Pacific (Tokyo) region has
entered the ALARM state, because "Threshold Crossed: 1 datapoint [9.0
(27/06/26 09:54:00)] was greater than or equal to the threshold (1.0)."

Alarm Details:
- Name:                       monitored-app-error-alarm
- Description:                [方式①] Lambda エラー検知アラーム
- State Change:               INSUFFICIENT_DATA -> ALARM
- Reason for State Change:    Threshold Crossed: ...
- Timestamp:                  Saturday 27 June, 2026 09:55:54 UTC
- AWS Account:                471112657080
- Alarm Arn:                  arn:aws:cloudwatch:...

Monitored Metric:
- MetricNamespace:            MonitoredApp
- MetricName:                 LambdaErrorCount
- Period:                     60 seconds
- Statistic:                  Sum
- TreatMissingData:           notBreaching

Backlog 課題の見え方(方式①)

  • 課題タイトル: ALARM: "monitored-app-error-alarm" in Asia Pacific (Tokyo)(固定)
  • 課題詳細: 上記 AWS アラーム通知テキストがそのまま入る
  • エラーログの内容: ❌ 含まれない
  • 優先度・担当者: ❌ 自動設定不可

方式②:Subscription Filter → 通知 Lambda → Backlog REST API

構成図

監視対象 Lambda
    ↓ ログ出力
CloudWatch Logs
    ↓ Subscription Filter("ERROR" を含む行をリアルタイム転送)
通知 Lambda
    ↓ ログをデコード・整形
Backlog REST API(POST /api/v2/issues)
    ↓
Backlog 課題登録(エラーログ内容つき)

通知 Lambda の実装

前回記事の通知 Lambda を Backlog API 向けに実装します。API キーは AWS Secrets Manager で管理し、コードには書きません。

import json, base64, gzip, boto3, os
import urllib.request, urllib.parse, urllib.error
from datetime import datetime, timezone

secrets_client = boto3.client('secretsmanager', region_name='ap-northeast-1')
_backlog_config = None

def get_backlog_config():
    global _backlog_config
    if _backlog_config is None:
        secret = secrets_client.get_secret_value(
            SecretId=os.environ['BACKLOG_SECRET_NAME']
        )
        _backlog_config = json.loads(secret['SecretString'])
    return _backlog_config

def lambda_handler(event, context):
    log_data = decode_log_data(event['awslogs']['data'])
    log_group  = log_data['logGroup']
    log_stream = log_data['logStream']
    timestamp  = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')

    error_events = [
        e for e in log_data['logEvents']
        if 'ERROR' in e.get('message', '')
    ]
    if not error_events:
        return {'statusCode': 200, 'body': 'No error logs'}

    config = get_backlog_config()
    issue_key = create_backlog_issue(config, log_group, log_stream, timestamp, error_events)
    print(f'Backlog 課題作成: {issue_key}')
    return {'statusCode': 200, 'issueKey': issue_key}

def create_backlog_issue(config, log_group, log_stream, timestamp, error_events):
    domain        = config['domain']
    api_key       = config['apiKey']
    project_id    = os.environ['BACKLOG_PROJECT_ID']
    issue_type_id = os.environ['BACKLOG_ISSUE_TYPE_ID']
    priority_id   = os.environ['BACKLOG_PRIORITY_ID']

    summary = f'[Lambda] エラー検知: {log_group} ({timestamp})'
    desc = '\n'.join([
        '## エラー検知通知',
        '',
        '|項目|値|',
        '|---|---|',
        f'|検知日時|{timestamp}|',
        f'|ロググループ|{log_group}|',
        f'|ログストリーム|{log_stream}|',
        f'|エラー件数|{len(error_events)} 件|',
        '',
        '## エラーログ(最大10件)',
        '',
        *[line for ev in error_events[:10]
          for line in ['```', ev.get('message', '').strip(), '```', '']],
    ])

    params = urllib.parse.urlencode({
        'projectId':    project_id,
        'summary':      summary,
        'issueTypeId':  issue_type_id,
        'priorityId':   priority_id,
        'description':  desc,
    }).encode('utf-8')

    req = urllib.request.Request(
        f'https://{domain}/api/v2/issues?apiKey={api_key}',
        data=params, method='POST',
        headers={'Content-Type': 'application/x-www-form-urlencoded'}
    )
    with urllib.request.urlopen(req, timeout=15) as res:
        return json.loads(res.read())['issueKey']

def decode_log_data(data):
    return json.loads(gzip.decompress(base64.b64decode(data)))

Backlog の ID を事前確認する

projectIdissueTypeId は数値 ID です。以下のコマンドで確認します。

DOMAIN="your-space.backlog.com"
API_KEY="your-api-key"

# プロジェクト ID
curl "https://${DOMAIN}/api/v2/projects?apiKey=${API_KEY}" | jq '.[] | {id, projectKey}'

# 課題種別 ID
curl "https://${DOMAIN}/api/v2/projects/PROJECT_KEY/issueTypes?apiKey=${API_KEY}" | jq '.[] | {id, name}'

# 優先度 ID
curl "https://${DOMAIN}/api/v2/priorities?apiKey=${API_KEY}" | jq '.[] | {id, name}'

動作検証結果

Lambda 実行から 約30秒で Backlog に課題が作成されました。

Lambda 実行:       2026-06-27 18:52:10 JST
Backlog 課題登録:  2026-06-27 18:52:40 JST(約30秒後)

Backlog 課題の見え方(方式②)

  • 課題タイトル: [Lambda] エラー検知: /aws/lambda/monitored-app (2026-06-27 09:52:04 UTC)(自由設定)
  • 課題詳細: エラーログの実内容が入る(下記)
  • 優先度: ✅ API で設定可能
  • 担当者・カテゴリ: ✅ API で自由に設定可能

課題詳細に記録されるエラーログ内容:

## エラー検知通知

|項目|値|
|---|---|
|検知日時|2026-06-27 09:52:04 UTC|
|ロググループ|/aws/lambda/monitored-app|
|ログストリーム|2026/06/27/[$LATEST]xxxx|
|エラー件数|3 件|

## エラーログ(最大10件)

[ERROR] 2026-06-27T09:52:04.523Z xxxx Database connection failed: timeout after 30s
[ERROR] 2026-06-27T09:52:04.523Z xxxx NullPointerException at module.process_data line 42
[ERROR] Exception: Application error: critical failure

両方式の比較

観点 方式① Alarm + SNS 方式② Subscription Filter + Lambda
通知までの時間 約2分(集計1分+評価1分) 約30秒
エラーログの内容 ❌ なし(アラームメタデータのみ) ✅ あり(実際のエラーメッセージ)
課題タイトル ALARM: "アラーム名" in リージョン(固定) 自由に設定可能
優先度・担当者 ❌ 設定不可 ✅ 設定可能
Lambda の必要性 ❌ 不要 ✅ 必要
実装コスト
ランニングコスト 低(Lambda 実行なし) 中(Lambda 実行課金)

選択基準

シンプルに始めたい / Lambda 管理コストを避けたい
    → 方式①
    ※ コンソールで CloudWatch Logs を別途確認する運用が前提

エラーの詳細を課題に入れたい / 優先度・担当者を自動設定したい
    → 方式②

ハマりポイントまとめ

1. Metric Filter パターンは "[ERROR]" で指定する

Lambda ログの実際のフォーマットは次のとおりです。

\t[ERROR]\t2026-06-27T09:54:00Z\txxxxxx\tDatabase connection failed

構造化パターン [timestamp, requestId, level="ERROR", ...] ではマッチしません。キーワード検索 "[ERROR]" を使ってください。

2. SNS 確認メールが Backlog に課題として登録される

SNS にメールアドレスをサブスクライブすると AWS から確認メールが届きます。
Backlog の課題登録用アドレスはどんなメールも課題として登録するため、確認メールも課題になります
これは避けられない動作なので、「最初の1件は確認メール由来の課題が作られる」ことを運用ルールとして周知しておくとよいでしょう。

3. Subscription Filter は1ロググループに1つのみ

1ロググループに設定できる Subscription Filter は1つだけです。
複数の通知先(Slack・Backlog など)に同時に送りたい場合は、通知 Lambda の中で複数の送信先を処理する設計にします。

4. 方式②は Lambda 実行のたびに課題が1件作られる

エラーが頻発すると課題が大量に作られるケースがあります。
対策として SQS バッファや重複チェック(同一ロググループの未完了課題を API で検索してスキップ)を実装することを検討してください。


まとめ

実際に Backlog へ連携して動作検証した結果をまとめます。

方式① 方式②
Backlog 課題の内容 アラーム通知のみ(エラーログなし) エラーログ本文込み
通知までの時間 約2分 約30秒
一言で シンプル・安い・情報が薄い 柔軟・リッチ・管理コストあり

Backlog でインシデント管理を完結させるなら方式②が適しています。 課題を見るだけでエラー内容が把握でき、対応スピードが上がります。

方式①は「まずアラートを受け取れればよい」という段階や、コスト・シンプルさを優先する場面で有効です。


参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?