New RelicとOpenAIを活用したエラーログ調査の自動化

こんにちは、香田です。

今回はNew RelicとOpenAIを活用したエラーログ調査の自動化について紹介していきます。

この記事はNew Relic Advent Calender 2023 シリーズ2の10日目の記事になります。

全体像について

今回紹介する構成の全体像は下記になります。

Lambda アプリケーションのエラーログをNew Relicでアラート検知し、アラート通知先としてEventBridgeを利用しLambda アプリケーションを起動します。

起動したLambda アプリケーションにて、エラーログ調査の自動化としてエラー対応方法をOpenAIへリクエストし、Slackへ通知する流れになります。

最終的にSlackへ下記のようなエラー内容とエラー対応方法を通知するイメージとなります。

OpenAI API キーの発行

エラーログの対応方法をOpenAIへリクエストする為、 APIキーを発行します。

APIキーの発行方法は、下記を参考に発行してみてください。

Slack Incoming Webhook URLの発行

Slack 通知用にIncoming Webhook URLを発行します。

Incoming Webhook URLの発行方法は、下記を参考に発行してみてください。

New Relic ログ転送の設定

次にCloudWatchのログをNew Relicへ転送する為、New Relicで提供されているnewrelic-log-ingestionを作成します。

AWS Serverless Application Repositoryを利用することで、簡単にデプロイ可能です。

下記のように[NewRelic-log-ingestion]を選択しデプロイしていきます。

New Relicへログが転送されるように、[アプリケーションの設定]より[NRLicenseKey]へライセンスキーの入力と、[NRLoggingEnabled]をTrueへ変更しデプロイしてください。

しばらくすると下記のようにnewrelic-log-ingestionが作成されているはずです。

New Relic アラートポリシーの作成

次にNew Relicでアラートポリシーを作成していきます。

[Alerts & AI]、[Alert Policies]よりアラートポリシーを作成します。

New Relic アラート条件の作成

[New alert conditions]より、NRQLを使用しアラート条件を作成します。

下記のNRQLを設定後、しきい値を適宜設定しアラート条件を作成します。

SELECT count(*) FROM Log 
FACET aws.logGroup, messageId 
WHERE message LIKE '[ERROR]%'

New Relic Destinationの作成

次にDestinationとしてEventBridgeを設定します。

[Alerts & AI]、[Destinations]より[AWS EventBridge]を選択します。

[Name]、[AWS region]、[AWS account ID]を適宜入力します。

New Relic Workflowの作成

次にWorkflowを作成していきます。

[Alerts & AI]、[Workflows]より通知先にEventBridgeを設定したWorkflowを作成します。

[Filter data]に作成したアラートポリシーを指定し、[state]が[ACTIVATED]を条件として追加します。

検知したエラーログを抽出し、エラーの内容をEventBridgeへ連携できるように[Additional settings]よりエンリッチメントを設定します。

クエリ名とNRQLを指定しログを抽出できるようにします。

  • クエリ名
cloudwatch-logs-message
  • NRQL
SELECT message FROM Log
WHERE aws.logGroup IS NOT NULL 
AND messageId = {{ accumulations.tag.messageId }}

NotifyにEventBridgeを指定し、EventBridgeにてイベントソースを関連付けしていきます。

EventBridgeの詳細な設定は下記も参考にしてみてください。

EventBridgeに通知するテンプレートとして、下記の内容を入力します。

下記のテンプレートで定義された内容を元に、Lambda アプリケーションでイベントデータとして受け取り通知する流れです。

  • Payload
{
    "id": {{ json issueId }},
    "issueUrl": {{ json issuePageUrl }},
    "priority": {{ json priority }},
    "state": {{ json state }},
    "trigger": {{ json triggerEvent }},
    "isCorrelated": {{ json isCorrelated }},
    "createdAt": {{ createdAt }},
    "updatedAt": {{ updatedAt }},
    "enrichData" : {{ json cloudwatch-logs-message }},
    "accumulations" : {{ json accumulations }}  
}

Workflowの作成により、New Relic側の準備は一通り完了です。

ローカル開発環境 セットアップ

次にLambda アプリケーション セットアップの為、Docker Composeを利用しローカル開発環境を準備していきます。

Lambda アプリケーションのデプロイはServleress Frameworkを利用していきます。

作業ディレクトリ作成

mkdir newrelic-openai
cd newrelic-openai

Docker Compose環境の作成

  • Dockerfile
FROM python:3.9

RUN apt-get update && \
    apt-get install -y nodejs npm && \
    rm -rf /var/lib/apt/lists/*
  • docker-compose.yml
version: "3.8"
services:
  app:
    build: .
    container_name: app
    working_dir: /usr/src/app
    tty: true
    volumes:
      - ./:/usr/src/app

コンテナ起動しログイン

docker compose up -d
docker compose exec app bash

openai パッケージ インストール

pip install --upgrade pip
pip install openai==0.27.0
pip freeze > requirements.txt

最新のopenai パッケージをLambdaで利用する場合、「No module named ‘pydantic_core._pydantic_core’」のエラーにより実行環境の問題が発生する為、ここでは問題なく動作するopenaiのバージョンを指定しています。

  • https://github.com/pydantic/pydantic/issues/6557
  • https://forum.serverless.com/t/no-module-named-pydantic-core-pydantic-core/19616

Serverless Framework インストール

npm install -g serverless

Serverless Frameworkの設定ファイル作成

  • serverless.yml
service: newrelic-openai
frameworkVersion: "3"

provider:
  name: aws
  runtime: python3.9
  stage: dev
  region: ap-northeast-1
  iamRoleStatements:
    - Effect: Allow
      Action:
        - logs:CreateLogStream
        - logs:PutLogEvents
      Resource:
        - "*"

plugins:
  # Python依存関係の管理
  - serverless-python-requirements
  # ログサブスクリプションプラグイン
  - serverless-plugin-log-subscription

custom:
  # New Relicのログ収集先ARN
  logSubscription:
    destinationArn: ${env:NEW_RELIC_LOG_INGESTION}
  # Pythonの依存関係をLambdaレイヤーとして使用
  pythonRequirements:
    layer: true

functions:
  # ログレポーター関数
  reporter:
    name: ai-log-reporter
    handler: ai-log-reporter/handler.lambda_handler
    # Python依存関係レイヤーの参照
    layers:
      - Ref: PythonRequirementsLambdaLayer
    # 環境変数
    environment:
      OPENAI_API_KEY: ${env:OPENAI_API_KEY}
      SLACK_WEBHOOK_URL: ${env:SLACK_WEBHOOK_URL}
    # イベントトリガー
    events:
      - eventBridge:
          # イベントバスの設定
          eventBus: ${env:EVENT_BUS}
          pattern:
            source:
              - prefix: "aws.partner/newrelic.com"
  # エラーログエミッター関数
  emitter:
    name: error-log-emitter
    handler: error-log-emitter/handler.lambda_handler
    # ログサブスクリプションの有効化
    logSubscription: true

Serverless Frameworkで利用するPlugin インストール

sls plugin install -n serverless-python-requirements
sls plugin install -n serverless-plugin-log-subscription

Lambda アプリケーション 作成

次にLambda アプリケーションを作成していきます。

アラート通知を検証できるように、エラー出力専用のLambda アプリケーションを作成します。

mkdir error-log-emitter
  • error-log-emitter/handler.py
def lambda_handler(event, context):
    try:
        # 存在しないインデックスにアクセスしエラーを発生させる
        data = [1, 2, 3]
        fourth_element = data[3]
        print(fourth_element)

    except Exception as e:
        raise

EventBridge経由で実行され、OpenAI APIへリクエストしSlack通知するLambda アプリケーションを作成します。

mkdir ai-log-reporter
  • ai-log-reporter/handler.py
import json
import openai
import os
import logging
import urllib.request

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

SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']
OPENAI_API_KEY = os.environ['OPENAI_API_KEY']
OPENAI_MODEL = "text-davinci-003"


def send_to_slack(payload: dict) -> None:
    """Slack通知の送信"""
    try:
        data = json.dumps(payload).encode('utf-8')
        req = urllib.request.Request(SLACK_WEBHOOK_URL, data=data, headers={
                                     'Content-Type': 'application/json'})
        with urllib.request.urlopen(req) as response:
            if response.status == 200:
                logger.info("Slack通知が送信されました")
            else:
                logger.error("Slack通知の送信に失敗しました: ステータスコード %s", response.status)
    except Exception as e:
        logger.exception("Slack通知の送信中にエラーが発生しました: %s", e)
        raise


def create_slack_payload(result: str,
                         message: str,
                         issue_url: str,
                         condition_name: str,
                         log_group: str) -> dict:
    """Slack通知用のペイロードを作成する"""
    return {
        "text": "*<!channel> New Relic アラート通知*",
        "attachments": [
            {
                "color": "#E01E5A",
                "blocks": [
                    {
                        "type": "section",
                        "text": {
                         "type": "mrkdwn",
                         "text": f"*アラート名*\n<{issue_url}|{condition_name}>"}
                    },
                    {
                        "type": "section",
                        "text": {
                            "type": "mrkdwn",
                            "text": f"*ロググループ名*\n {log_group}"
                        }
                    },
                    {
                        "type": "section",
                        "text": {
                            "type": "mrkdwn",
                            "text": f"*エラー内容*\n {message}"
                        }
                    },
                    {
                        "type": "section",
                        "text": {
                            "type": "mrkdwn",
                            "text": f"*エラー対応方法*\n {result}"
                        }
                    },
                ]
            }
        ]
    }


def lambda_handler(event, context):
    # OpenAI APIキーの設定
    openai.api_key = OPENAI_API_KEY
    try:
        # EventBriteのイベント情報を取得
        message = event['detail']['enrichData']['result'][0]['message']
        issue_url = event['detail']['issueUrl']
        condition_name = event['detail']['accumulations']['conditionName'][0]
        log_group = event['detail']['accumulations']['tag']['aws']['logGroup'][0]
        # OpenAI APIの呼び出し
        response = openai.Completion.create(
            model=OPENAI_MODEL,
            prompt=f"このエラーに対してどういった対応が必要ですか?: '{message}'",
            max_tokens=1000,
        )
        # OpenAI API 回答結果の取得
        result = response.choices[0].text.strip()
        # Slack通知の送信
        payload = create_slack_payload(
            result, message, issue_url, condition_name, log_group)
        send_to_slack(payload)
    except Exception as e:
        logger.exception("Lambda関数の実行中にエラーが発生しました: %s", e)
        raise

Lambda アプリケーション デプロイ

次にLambda アプリケーションをデプロイしていきます。

デプロイする為に必要となる環境変数を設定していきます。

AWS環境 認証情報の設定

export AWS_ACCESS_KEY_ID=xxxx
export AWS_SECRET_ACCESS_KEY=xxxx
export AWS_DEFAULT_REGION=ap-northeast-1

EventBridgeで作成したイベントバスのARN設定

export EVENT_BUS=<EventBridge イベントバス ARN>

newrelic-log-ingestionのARNの設定

export NEW_RELIC_LOG_INGESTION=<Lambda newrelic-log-ingestion ARN>

OpenAI API キーの設定

export OPENAI_API_KEY=<OpenAI API キー>

Slack Webhook URLの設定

export SLACK_WEBHOOK_URL=<Slack Incoming Webhook URL>

Lambda アプリケーションのデプロイ

sls deploy --verbose

デプロイが成功すると、下記のようにLambda アプリケーションが確認できるはずです。

エラーログのアラート通知確認

動作確認として、エラー出力専用のLambda アプリケーションを実行しエラーを擬似的に出力させます。

sls invoke --function emitter

New Relic Logsに実行したLambda アプリケーションのログが、下記のように転送されているか確認します。

エラー発生によって、しばらくするとエラーログに対するアラートが検知されるはずです。

アラート検知によりEventBrdige経由でLambda アプリケーションが起動し、下記のようにSlakcへ通知されていれば成功です。

クリーンアップ

作成した環境が不要になったら、下記でクリーンアップしてください。

デプロイしたLambda アプリケーションの削除

sls remove --verbose

ローカル開発環境として作成したDocker Compose環境の削除

docker compose down --rmi all --volumes --remove-orphans

さいごに

New RelicとOpenAIを活用したエラーログ調査の自動化方法いかがでしたでしょうか。

New RelicとOpenAIを組み合わせることで、エラー調査のステップが簡略化できるのではないでしょうか。

最後までご覧いただきありがとうございます。

SNSでもご購読できます。