検索・集計がしやすいロギング環境の構築

調査しやすいロギング環境について社内勉強会で発表したやつをまとめたやつです。


問題が起きたときなどにログを調査することはよくあることだが、ここでは調査しやすログとはどのようなもので、どうやって構築していくかについて述べる。

何も考えずにロギングしたときの問題点

  • ログファイルが巨大すぎて大変
  • ログファイルが複数に大量に分かれて大変
  • 複数のリクエストのログが混ざって識別が困難
  • ログを検索するときに正規表現などで検索しなければならず大変

調査や集計などがしやすいロギング環境への改善

上記の問題を解決するためには以下を行う。

  • ログをローテーションする
  • ログを集約する
  • リクエストを判別できるリクエストIDなどを付与する
  • ログをJSONなどに構造化する

ログをローテーションする

  • ActiveSupport::Loggerlogrotate を使う

ログを集約する

  • CloudWatch Logs / CloudWatch Logs agent などを使って集約する

リクエストを判別できるリクエストIDなどを付与する

  • リクエストIDとはリクエストごとに発行される一意なIDのこと
  • これをロギング時に付与することにより、リクエストレベルで検索可能
    • サーバーは複数あったり、マルチプロセス/マルチスレッドでリクエストを複数同時に処理するため、各リクエストのログが混ざってよくわからなくなりがち

よいこと

  • 検索性が上がる
  • 集計しやすくなる

わるいこと

  • ログサイズが膨れがち
  • 付与する情報が多いと人間が見にくくなる

Railsでの実現はどうするか?

  • ActiveSupport::TaggedLogging によって可能
  • デフォルトだとproduction環境で付与するようになっているはず
ActiveSupport::TaggedLogging とは
  • 任意のLoggerオブジェクト に tagged(*tags) というメソッドを追加するもの
  • このメソッドによりメッセージにタグを付与することが可能
logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
logger.info 'baz' #=> baz
logger.tagged('hoge') { logger.info 'baz' } #=> [hoge] baz
logger.tagged('hoge','foo') { logger.info 'baz' } #=> [hoge] [foo] baz
logger.info 'baz' #=> baz
リクエストIDを付与する

config.log_tags を設定することにより出力可能。ここで設定された内容が Rails::Rack::Logger にて ActiveSupport::TaggedLogging#tagged(*tags) メソッドに渡されリクエスト毎にタグが付与できる。

config/application.rb:

module HogeApp
  class Application < Rails::Application
    ...
    config.log_tags = [ :request_id ]
  end
end
[8075c9d7-ea51-4069-bba0-e5251a5f4763] Processing by Rails::WelcomeController#index as HTML
[8075c9d7-ea51-4069-bba0-e5251a5f4763]   Rendering /usr/local/bundle/gems/railties-6.0.3/lib/rails/templates/rails/welcome/index.html.erb
[8075c9d7-ea51-4069-bba0-e5251a5f4763]   Rendered /usr/local/bundle/gems/railties-6.0.3/lib/rails/templates/rails/welcome/index.html.erb (Duration: 6.8ms | Allocations: 294)
[8075c9d7-ea51-4069-bba0-e5251a5f4763] Completed 200 OK in 12ms (Views: 9.6ms | ActiveRecord: 0.0ms | Allocations: 1539)

[66587c5c-5b40-41cf-a6ab-dc991bfb9f61] Started GET "/" for 172.29.0.1 at 2020-05-11 08:31:51 +0000
[66587c5c-5b40-41cf-a6ab-dc991bfb9f61] Cannot render console from 172.29.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
[66587c5c-5b40-41cf-a6ab-dc991bfb9f61] Processing by Rails::WelcomeController#index as HTML
[66587c5c-5b40-41cf-a6ab-dc991bfb9f61]   Rendering /usr/local/bundle/gems/railties-6.0.3/lib/rails/templates/rails/welcome/index.html.erb
[66587c5c-5b40-41cf-a6ab-dc991bfb9f61]   Rendered /usr/local/bundle/gems/railties-6.0.3/lib/rails/templates/rails/welcome/index.html.erb (Duration: 6.3ms | Allocations: 179)
[66587c5c-5b40-41cf-a6ab-dc991bfb9f61] Completed 200 OK in 10ms (Views: 8.6ms | ActiveRecord: 0.0ms | Allocations: 784)

ここで config.log_tags では、以下コードに示すとおり、シンボル、Procオブジェクト、文字列などが利用できる。

module Rails
  module Rack
    class Logger < ActiveSupport::LogSubscriber
      ...

      def call(env)
        request = ActionDispatch::Request.new(env)

        if logger.respond_to?(:tagged)
          logger.tagged(compute_tags(request)) { call_app(request, env) }
        else
          call_app(request, env)
        end
      end

      ...

      def compute_tags(request) # :doc:
        @taggers.collect do |tag|
          case tag
          when Proc
            tag.call(request)
          when Symbol
            request.send(tag)
          else
            tag
          end
        end
      end

      ...
    end
  end
end

https://github.com/rails/rails/blob/v6.0.3/railties/lib/rails/rack/logger.rb

  • シンボルを渡すと、 ActionDispatch::Request オブジェクトのメソッドを呼び、その戻り値がタグとして利用される
  • Proc オブジェクトを渡すと ActionDispatch::Request オブジェクトが渡され、呼び出し時の戻り値がタグとして利用される
  • それ以外の文字列などはそのままタグとして利用される

これを踏まえると、以下のように設定することもできる。

module HogeApp
  class Application < Rails::Application
    ...
    config.log_tags = [ 'Server', :method, ->(req){ URI.parse(req.fullpath).path }, :request_id ]
  end
end

結果は以下のようになる。

[Server] [GET] [/] [8075c9d7-ea51-4069-bba0-e5251a5f4763] Processing by Rails::WelcomeController#index as HTML
[Server] [GET] [/] [8075c9d7-ea51-4069-bba0-e5251a5f4763]   Rendering /usr/local/bundle/gems/railties-6.0.3/lib/rails/templates/rails/welcome/index.html.erb
[Server] [GET] [/] [8075c9d7-ea51-4069-bba0-e5251a5f4763]   Rendered /usr/local/bundle/gems/railties-6.0.3/lib/rails/templates/rails/welcome/index.html.erb (Duration: 6.8ms | Allocations: 294)
[Server] [GET] [/] [8075c9d7-ea51-4069-bba0-e5251a5f4763] Completed 200 OK in 12ms (Views: 9.6ms | ActiveRecord: 0.0ms | Allocations: 1539)

[Server] [GET] [/] [66587c5c-5b40-41cf-a6ab-dc991bfb9f61] Started GET "/" for 172.29.0.1 at 2020-05-11 08:31:51 +0000
[Server] [GET] [/] [66587c5c-5b40-41cf-a6ab-dc991bfb9f61] Cannot render console from 172.29.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
[Server] [GET] [/] [66587c5c-5b40-41cf-a6ab-dc991bfb9f61] Processing by Rails::WelcomeController#index as HTML
[Server] [GET] [/] [66587c5c-5b40-41cf-a6ab-dc991bfb9f61]   Rendering /usr/local/bundle/gems/railties-6.0.3/lib/rails/templates/rails/welcome/index.html.erb
[Server] [GET] [/] [66587c5c-5b40-41cf-a6ab-dc991bfb9f61]   Rendered /usr/local/bundle/gems/railties-6.0.3/lib/rails/templates/rails/welcome/index.html.erb (Duration: 6.3ms | Allocations: 179)
[Server] [GET] [/] [66587c5c-5b40-41cf-a6ab-dc991bfb9f61] Completed 200 OK in 10ms (Views: 8.6ms | ActiveRecord: 0.0ms | Allocations: 784)

ログをJSONなどに構造化する

  • いわゆる構造化ロギングで、JSONなどの構造化されたフォーマットでロギングすること

よいこと

  • 検索性が上がる
  • 集計しやすくなる
  • プログラムで処理しやすい

わるいこと

  • ログサイズが膨れがち
  • 人間にとっては見にくい

Railsでの実現はどうするか?

JSONに出力するFormatterを使えばいいが、タグを付与する ActiveSupport::TaggedLogging で少々問題になる。 たとえば、 ActiveSupport::TaggedLogging で使用する Formatter では、タグとメッセージを連結してからオリジナルのFormatterを呼び出している。

  module TaggedLogging
    module Formatter # :nodoc:
      # This method is invoked when a log event occurs.
      def call(severity, timestamp, progname, msg)
        super(severity, timestamp, progname, "#{tags_text}#{msg}")
      end
      ...

https://github.com/rails/rails/blob/v6.0.3/activesupport/lib/active_support/tagged_logging.rb#L22-L24

このままだと、タグ情報を構造化してログ出力できない。そのため、以下のように ActiveSupport::TaggedLogging を拡張するラッパークラスを作成する。

module Logging
  module JsonStructuredTaggedLogging
    module Formatter
      def call(severity, timestamp, progname, message)
        result = {
          severity: severity.to_s,
          timestamp: timestamp.iso8601,
          progname: progname.to_s,
          tags: current_tags.map(&:to_s),
          message: message.to_s,
        }
        result = result.to_json + "\n"
        result
      end
    end

    def self.new(logger)
      logger = ActiveSupport::TaggedLogging.new(logger) unless logger.respond_to?(:tagged)
      logger.formatter.extend Formatter
      logger.extend self
      logger
    end
  end
end

ここまでできたら、 Rails.logger などで使用していく。

config/application.rb:

class Application < Rails::Application
  ...
  config.logger = Logging::JsonStructuredTaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
end

ロギングをよりよくする

Lograge を使ってリクエスト毎の情報をまとめてロギングする

  • リクエスト単位で実行時間やエラーなどのログを出力できるので便利
  • lograge のログにリクエストIDを出力するようにするとRails標準のログと照会できて便利

config/initializers/lograge.rb:

Rails.application.configure do
  config.lograge.enabled = true
  config.lograge.keep_original_rails_log = true
  config.lograge.logger = ActiveSupport::Logger.new(STDOUT)
  config.lograge.formatter = Lograge::Formatters::Json.new

  config.lograge.custom_options = lambda do |event|
    data = {
      request_id: event.payload[:request_id],
    }
    data
  end
end

app/controllers/application_controller.rb:

class ApplicationController < ActionController::Base
  protected

  def append_info_to_payload(payload)
    super
    payload[:request_id] = request.request_id
  end
end

rescue_from などでエラーを補足したときは必ずログにエラー内容を出力する

  • なにが起きたのかがわからなくなるため
  • エラーだけでなく前後でなにが起きたのかも見たいので、Sentry などのエラートラッキングサービスをつかっていても出力する
  • lograge を使っている場合は、例外オブジェクトを append_info_to_payload で追加する

app/controllers/application_controller.rb:

class ApplicationController < ActionController::Base
  rescue_from StandardError, with: :error_500
  rescue_from ActiveRecord::RecordNotFound, with: :error_404

  protected

  def error_404(e)
    Rails.logger.warn e.inspect
    @exception = e
    render plain: 'error 404'
  end

  def error_500(e)
    Rails.logger.error [e.inspect, *e.backtrace].join("\n")
    Raven.capture_exception(e) # Sentry に通知
    @exception = e
    render plain: 'error 500'
  end

  # action => rescue_from => append_info_to_payload の順で呼ばれる
  def append_info_to_payload(payload)
    super
    payload[:request_id] = request.request_id
    if @exception.present?
      payload[:exception_object] ||= @exception
      payload[:exception] ||= [@exception.class.name, @exception.message]
    end
  end
end

config/initializers/lograge.rb:

Rails.application.configure do
  ...
  config.lograge.custom_options = lambda do |event|
    data = {
      request_id: event.payload[:request_id],
    }
    if event.payload[:exception].present?
      data[:exception] = event.payload[:exception]
      data[:exception_backtrace] = event.payload[:exception_object]&.backtrace
    end
    data
  end
end

バックグラウンドjobやrakeタスクなども job id や task id などを出力して検索しやすくする

  • [Rake][hoge_task][xxx-bbb-ddd] みたいな感じでタグを付与する
  • これによってログの調査がしやすくなる
  • IDはUUID v4を使うと楽
    • SecureRandom.uuid

ActiveJob

  • ActiveJobはJobIDをログに付与する

[ActiveJob] もしくは [ActiveJob] [<job-name>] [<job-id>] というプレフィックスがつく

[ActiveJob] Enqueued ExampleJob (Job ID: 6b7b2799-3479-4647-a5cf-b9b371b7e7bb) to Sidekiq(default)
[ActiveJob] [ExampleJob] [6b7b2799-3479-4647-a5cf-b9b371b7e7bb] Performing ExampleJob (Job ID: 6b7b2799-3479-4647-a5cf-b9b371b7e7bb) from Sidekiq(default) enqueued at 2020-05-11T09:01:14Z
[ActiveJob] [ExampleJob] [6b7b2799-3479-4647-a5cf-b9b371b7e7bb] ### test
[ActiveJob] [ExampleJob] [6b7b2799-3479-4647-a5cf-b9b371b7e7bb] Performed ExampleJob (Job ID: 6b7b2799-3479-4647-a5cf-b9b371b7e7bb) from Sidekiq(default) in 0.92ms

Rake タスクなど

ActiveSupport::TaggedLogging#tagged(*tags) を利用して以下のようにする。

namespace :example do
  logging = ->(type, &block) {
    Rails.logger.tagged('Rake', "example:#{type}", SecureRandom.uuid, &block)
  }
  task :exec => [:environment] do
    logging[:exec] do
      Rails.logger.info 'test'
    end
  end
end
$ rails example:exec
$ cat log/development.log
[Rake] [example:exec] [e64f4190-1c70-4ff0-9a4d-c913c5e77ec4] test

構造化されたログの型付け

Rails が出力するログのほかにも様々なログがあるので、ログの型を決めておくとあとで検索がしやすい。 そのため型付けをどうするかについて述べる。

まず、すべての構造化ログは最低でも以下のプロパティを持つものとする。

type LogBase = {
  type: 'rails' | 'lograge';
  level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL' | 'ANY';
  time: string; // ISO 8601
};

そして Rails 標準のログを以下のように定義する。

type RailsLog = LogBase & {
  sub_type: 'server' | 'job' | 'other';
  request_id: string | null; // for server
  job_name: string | null; // for active job
  job_id: string | null; // for active job
  message: string;
  tags: Array<string>;
};

次に Lograge のログはユーザで拡張できるので拡張部分と基本部分をあわせて以下のように定義する。

type LogrageBaseLog = {
  method: string;
  path: string;
  format: string;
  controller: string;
  action: string;
  status: number;
  duration: number;
  view: number;
  db: number;
}
type LogrageAppendedLog = LogBase & {
  request_id: string;
  exception: [string, string] | null;
  exception_backtrace: string[] | null;
};
type LogrageLog = LogrageLog & LogrageAppendedLog;

つまり出力されるログの型は以下のようになる。共通の type により、ログの型が一意に決まる。

type Log = RailsLog | LogrageLog;

これらを踏まえて、Ruby でログイベントを扱うクラスを作る。

module Logging
  module Entity
    class LogBase
      TYPES = %i(rails lograge).freeze
      LEVELS = Logger::SEV_LABEL.freeze

      def self.fetch_type(value)
        Hash[*TYPES.map {|v| [v, v] }.flatten].fetch(value.to_s.intern)
      end

      def self.fetch_level(value)
        Hash[*LEVELS.map {|v| [v, v] }.flatten].fetch(value.to_s.upcase)
      end

      attr_reader :type
      attr_reader :time
      attr_reader :level

      def initialize(type:, level:, time:)
        @type = self.class.fetch_type(type)
        @time = time.iso8601(6)
        @level = self.class.fetch_level(level)
      end
    end

    class RailsLog < LogBase
      SUB_TYPES = %i(server job other)

      def self.fetch_sub_type(value)
        Hash[*SUB_TYPES.map {|v| [v, v] }.flatten].fetch(value.to_s.intern)
      end

      attr_accessor :sub_type
      attr_accessor :tags
      attr_accessor :request_id
      attr_accessor :job_name
      attr_accessor :job_id
      attr_accessor :message

      def initialize(time:, level:, message:, tags:)
        super(type: :rails, time: time, level: level)
        @sub_type = nil
        @tags = tags
        @request_id = nil
        @job_name = nil
        @job_id = nil
        @message = message

        case @tags[0]
        when 'Server'
          @sub_type = self.class.fetch_sub_type(:server)
          @request_id = @tags[1]
        when 'ActiveJob'
          @sub_type = self.class.fetch_sub_type(:job)
          if @tags.size >= 3
            @job_name = @tags[1]
            @job_id = @tags[2]
          end
        else
          @sub_type = self.class.fetch_sub_type(:other)
        end
      end
    end

    class LogrageAppendedLog < LogBase
      attr_reader :request_id
      attr_reader :exception
      attr_reader :exception_backtrace

      def initialize(
        time:,
        level:, 
        request_id:,
        exception: nil,
        exception_backtrace: nil
      )
        super(type: :lograge, time: time, level: level)
        @request_id = request_id
        @exception = exception
        @exception_backtrace = exception_backtrace
      end
    end
  end
end

JSONで構造化するロギングクラスで使い、指定の形式で出力する。

module Logging
  module JsonStructuredTaggedLogging
    module Formatter
      def call(severity, timestamp, _progname, message)
        result = Logging::Entity::RailsLog.new(
          time: timestamp,
          level: severity,
          message: message,
          tags: current_tags
        )
        result = result.to_json + "\n"
        result
      end
    end
    ...
end
Rails.application.configure do
  ...

  config.lograge.custom_options = lambda do |event|
    data = {
      time: event.payload[:time],
      level: event.payload[:level] || 'INFO',
      request_id: event.payload[:request_id],
    }
    if event.payload[:exception].present?
      data[:level] = event.payload[:level] || 'FATAL'
      data[:exception] = event.payload[:exception]
      data[:exception_backtrace] = event.payload[:exception_object]&.backtrace
    end
    Logging::Entity::LogrageAppendedLog.new(**data).as_json
  end
end

ログの検索・集計

CloudWatch Logs で集約・検索

  • AWSを使っているならCloudWatch Logs を使う
  • JSONによる構造化ロギングをサポートしている
  • JSON メトリクスフィルターによる高度な検索

JSON メトリクスフィルターによる高度な検索

JSON メトリクスフィルターとは以下のような構文で、JSONログイベントの特定のキーの値に対する条件を記述することができる。

{ SELECTOR EQUALITY_OPERATOR STRING }

例えば、以下のようなログイベントがあったとして、

{ "type": "type1", "message": "message 1" }
plain text
{ "type": "type1", "message": "message 2" }
{ "type": "type2", "message": "message 3" }

次のような構文で検索すると、

{ $.type = "type1" }

JSON ログイベントかつ typetype1 のもののみ抽出できる。

{ "type": "type1", "message": "message 1" }
{ "type": "type1", "message": "message 2" }

CloudWatch Logs Insights による高度な検索

CloudWatch Logs で JSONメトリクスフィルターを使って高度な検索ができるが、 CloudWatch Logs Insights というサービスで、SQLライクな構文をつかってより高度な検索が可能になる。

fields @timestamp, path, request_id, exception.1
| sort @timestamp desc
| filter type = "lograge" AND level = "WARN"
| limit 20

f:id:mrk21:20200706190127p:plain

S3へのエクスポートとAthenaによる高度な検索・集計

  • Cloudwatch Logs は Insights 含め、検索速度が遅く、ストレージ料金も割高である
    • あくまでも一時的なログ集約場として扱うほうがよい
    • 1,2週間でexpireするのがよい
  • Lambdaを書き、日次でS3に書き出し
  • Athenaを使う
    • Athena とは S3 にあるログファイルをSQLによって検索・集計することができる
    • ログファイルが膨大になってくるとスキャン時の課金が多くなるので、パーティションに分ける
      • Lambdaで日次でやる

S3へのエクスポート

Amazon Kinesis Data Firehose というサービスを使えばフルマネージドにS3にエクスポートできるが料金が高いのでLambdaとCloudWatch Eventsを用いて行う。

Lambda を Node.js で実装する場合 ADS SDKaws.CloudWatchLogs#createExportTask() を用いて指定のS3バケットにログをエクスポートする。

// @see [Lambda + S3 で、CloudWatch Logsのログをログローテーションする - Qiita](https://qiita.com/tamura_CD/items/3a1deba9e55e8b83bd91)
// @see [Class: AWS.CloudWatchLogs — AWS SDK for JavaScript](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudWatchLogs.html#createExportTask-property)
'use strict';

const aws = require('aws-sdk');
aws.config.update({ region: 'ap-northeast-1' });

exports.handler = (event, context, callback) => {
  const cloudWatchLogs = new aws.CloudWatchLogs();
  const range = getRange();
  const params = {
    destination: process.env.BucketName,
    from: range.from,
    to: range.to,
    logGroupName: process.env.LogGroupName,
    destinationPrefix: `${process.env.DestinationPrefix}/dt=${range.date}`, // hive形式で出力
    taskName: `${process.env.TaskName}/${range.date}`,
  };
  console.log(`params: ${JSON.stringify(params)}`);

  cloudWatchLogs.createExportTask(params, (error, data) => {
    if (error) {
      console.log(error, error.stack);
      callback(null, error.stack);
    }
    else {
      console.log(data);
      callback(null, data);
    }
  });
};

function getRange() {
  const result = {};
  const time = new Date();
  time.setTime(time.getTime() + 9 * 60 * 60 * 1000); // JST

  time.setDate(time.getDate() - 2);
  time.setHours(0);
  time.setMinutes(0);
  time.setSeconds(0);
  time.setMilliseconds(0);
  result.from = time.getTime(); // 前日の日本時間 00:00:00 のタイムスタンプ(含ミリ秒)

  time.setDate(time.getDate() + 1);
  result.to = time.getTime(); // 本日の日本時間 00:00:00 のタイムスタンプ(含ミリ秒)

  const yyyy = time.getFullYear();
  const mm = ('0' + (time.getMonth() + 1)).slice(-2);
  const dd = ('0' + time.getDate()).slice(-2);
  result.date = `${yyyy}-${mm}-${dd}`; // yyyy-mm-dd のフォーマット

  return result;
}

しかし、この方法でエクスポートすると timestamp が JSONログの前に付与されてしまうので、valid な JSON でなくなってしまう。

2020-05-18T00:00:01.383Z {"type":"rails", ...}
2020-05-18T00:00:01.384Z {"type":"rails", ...}

Athena のテーブルを作成し、検索できるようにする

まずはデータベースを作成する。

CREATE DATABASE ecs_test;

テーブルを作成するが、上述したとおり valid な JSON ではなくなってしまっている。そのため、まずは plain text なカラムが一つあるテーブルを作成する。 Athena はスキャンしたデータ量に応じて課金されるため、日付でパーティションを切る。

CREATE EXTERNAL TABLE IF NOT EXISTS ecs_test.raw_app_logs (`text` string)
PARTITIONED BY (`dt` date)
LOCATION 's3://test-logs/ecs-test/'
TBLPROPERTIES ('has_encrypted_data'='false');
MSCK REPAIR TABLE ecs_test.raw_app_logs;

そして、次にその plain text から JSON を抜き出して各プロパティにアクセス可能にするため、以下の view を作成する。JSONのままでもAthenaにはJSON関数があるので特定のプロパティの値を参照できる。

CREATE OR REPLACE VIEW ecs_test.json_app_logs AS
SELECT
  dt,
  CAST(from_iso8601_timestamp(timestamp) AS TIMESTAMP) AS timestamp,
  CAST(json_extract(json, '$.type') AS VARCHAR) AS type,
  CAST(json_extract(json, '$.level') AS VARCHAR) AS level,
  CAST(from_iso8601_timestamp(CAST(json_extract(json, '$.time') AS VARCHAR)) AS TIMESTAMP) AS time,
  json
FROM (
  SELECT
    dt,
    regexp_extract(text, '^([^ ]+) ({.+})$', 1) AS timestamp,
    regexp_extract(text, '^([^ ]+) ({.+})$', 2) AS json
  FROM raw_app_logs
)
WHERE
  json IS NOT NULL AND
  timestamp IS NOT NULL

このようにすることで、高度な検索・集計などができるようになる。

SELECT
  dt
, type
, level
, time
, CAST(json_extract(json, '$.request_id') AS VARCHAR) AS request_id
, CAST(json_extract(json, '$.message') AS VARCHAR) AS message
FROM json_app_logs
WHERE type = 'rails'
  AND dt = date '2020-05-20'

f:id:mrk21:20200706190121p:plain

最後に

ここまで述べてきたとおり、ログに原因特定のためのメタ情報を付与して、JSON等で構造化することによって、検索・集計がしやすくなる。 また、CloudWatch Logs などに集約し、Athena などでより高度な検索もできるようになる。

最後に、以下に本エントリーのための検証プロジェクトを示す。