filebeats でNginxのログを確認する

Ingest nodeと、FileBeatのモジュールがで利用できるようになって、とりあえず小規模でとりあえず導入するような構成ならLogstashとか、Fluentd無しのFilebeat + Elasticsearch + Kibanaの構成でなんとかなるんじゃ無いかと試してみる。
ある程度以上の規模になったり、欠損が許容されないような場合は、Logstashとか、FluentdのようなAggregatorを間に挟んだほうが当然良いと思う。

Filebeatsをインストールする

公式サイトのインストール方法を参考にインストールする。

www.elastic.co

Nginx Moduleで出力するIndexを分ける

たとえばNginx用のIndexを分ける場合は以下のような設定をする。
prospectorでfield を追加して、output側で特定の条件(fields.typeが"accesslog"の場合)にnginx-%{+yyyy.MM.dd}という indexに送信するとしている。
ただし、nginx.access.geoipのマッピングをする必要が出てくるので、特に問題がなければデフォルトで提供されているfilebeat-* のIndexを使う方が手間は少ない。

filebeat.modules:
- module: nginx
  # Access logs
  access:
    enabled: true
    var.paths:
      - /var/log/nginx/access.log
    prospector:
      fields:
        type: accesslog

  # Error logs
  error:
    enabled: true
    var.paths:
      - /var/log/nginx/error.log
    prospector:
      fields:
        type: "accesslog"


output.elasticsearch:
  enabled: true

  hosts: ["リモートホスト:9200"]
  index: "filebeat-%{+yyyy.MM.dd}"
  indices:
    - index: "nginx-%{+yyyy.MM.dd}"
      when.equals:
        fields.type: "accesslog"

Nginx ModuleとSyslog Moduleで出力するIndexを分ける

Nginx Moduleはデフォルトのfilebeat-* に出力し、Syslog Moduleの出力先のみ変更する。

filebeat.modules:
- module: system
  # Syslog
  syslog:
    enabled: true
    var.paths: ["/var/log/secure"]
    prospector:
      fields:
        app: "syslog"

filebeat.modules:
- module: nginx
  # Access logs
  access:
    enabled: true
    var.paths:
      - /var/log/nginx/access.log

  # Error logs
  error:
    enabled: true
    var.paths:
      - /var/log/nginx/error.log


output.elasticsearch:
  enabled: true

  hosts: ["リモートホスト:9200"]
  index: "filebeat-%{+yyyy.MM.dd}"
  indices:
    - index: "syslog-%{+yyyy.MM.dd}"
      when.equals:
        fields.app: "syslog"
    - index: "filebeat-%{+yyyy.MM.dd}"
      default:

結論

デフォルトで用意されているDashboardは、一からダッシュボードを作らなくて良いのでこんなことが出来るというという取っ掛かりには良い。         filebeat-* になんでも入れると、ダッシュボードが上手く動かないとか出て来るので最低限でもモジュール単位ぐらいでは Index を分けたくなるが、そのあたりを filebeat だけでやる情報が中々なくて手間取ったが、そこそこ使える感じにはなった。         比較的小規模でAWSなどでCPUクレジットがあるようなインスタンスを使っている場合は、filebeatだけで済むのでCPU負荷が低くてすむという利点もある。

Prometheusのec2 service discoveryを試す

Prometheusには ec2 service discovery機能があり、EC2インスタンスに監視用のagent(exporter) をインストールして、特定のタグを設定するだけで監視・モニタリングの対象とすることができます。

通常のノードの追加

設定方法は以下のように、対象となるノードの情報をPrometheusの設定ファイルに書き、設定を反映されるためPrometheusを再起動します。
これの作業はノードを追加するたびに発生します。

  - job_name: 'node'
    scrape_interval: 30s
    scrape_timeout:  15s
    static_configs:
      - targets: ['172.0.0.3:9100', '172.0.0.3:9256']
        labels:
          name: 'server1'
          stage: 'prod'

ec2 service discoveryを利用したノードの追加方法

ec2 service discoveryを利用するには設定ファイルを以下のように書きます。
ec2 service discoveryを利用することで、この例ではインスタンスのタグ(Stage)に、prodかstgを値と設定すると自動的に監視・モニタリング対象として自動的に検出します。

  - job_name: 'node'
    ec2_sd_configs:
      - region: ap-northeast-1
        access_key: APIKeyを書く
        secret_key: SECRET_KEYを書く
        port: 9100
    relabel_configs:
      - source_labels: [__meta_ec2_tag_Stage]
        regex: (stg|prod)
        action: keep
      - source_labels: [__meta_ec2_tag_Name]
        target_label: name
      - source_labels: [__meta_ec2_tag_Stage]
        target_label: stage

job_name はPrometheusでよく設定される監視対象をグルーピングするラベルです。
ec2_sd_configsにある設定項目は Configuration | Prometheus で確認できます。
ここでは最低限のAWSの設定と、Node exporter がリクエストを受け付ける port番号の設定のみをしています。

relabel_configsは以下のようになっています。

1. 最初の source_labels

監視・モニタリング対象とするかどうかの判断をします。この例ではEC2インスタンスのTag(Stage) の値が、stgかprodの場合は監視・モニタリング対象になります。

2. 2番目と3番目の source_labels

EC2インスタンスのTagの値をPrometheusのラベルに設定しています。
ここでは、EC2のタグのNameの値をPrometheusのラベルのnameに設定し、同様にStageの値をstageに設定しています。
このようにPrometheusのラベルに値を設定しておかないと、Prometheus内(Alertmanager含む)で値を利用できないためこのように設定します。

relabel_configsの設定は EC2 Discovery Relabelling - Robust Perception が参考になりました。

複数の exporter を利用する場合

監視に利用する exporter が複数の場合は、port を複数設定する必要が出てきますが、ec2_sd_configs では port は int値のみ指定可能で複数のportをまとめて書くことはできません。
そのため Node exporter と Process exporter を利用するような場合には、以下のように ec2_sd_configs を書く2つ書くことになります。

  - job_name: 'node'
    ec2_sd_configs:
      - region: ap-northeast-1
        access_key: APIKeyを書く
        secret_key: SECRET_KEYを書く
        port: 9100
    relabel_configs:
      - source_labels: [__meta_ec2_tag_Stage]
        regex: (stg|prod)
        action: keep
      - source_labels: [__meta_ec2_tag_Name]
        target_label: name
      - source_labels: [__meta_ec2_tag_Stage]
        target_label: stage

  - job_name: 'process'
    ec2_sd_configs:
      - region: ap-northeast-1
        access_key: APIKeyを書く
        secret_key: SECRET_KEYを書く
        port: 9256
    relabel_configs:
      - source_labels: [__meta_ec2_tag_Stage]
        regex: (stg|prod)
        action: keep
      - source_labels: [__meta_ec2_tag_Name]
        target_label: name
      - source_labels: [__meta_ec2_tag_Stage]
        target_label: stage

Swift3でファイルを読み込む

swiftファイルと同じ場所にある、sample.txtを読み込むコードは以下のようになる。

        let path = Bundle.main.path(forResource: "sample", ofType: "text")!
        if let data = NSData(contentsOfFile: path){
            print(String(NSString(data: data as Data, encoding: String.Encoding.utf8.rawValue)!))
        }else{
            print("データなし")
        }

Capistrano3のコードを読んでみる1 ~設定ファイルのパス設定~

Capistranoを実行時に利用するconfig/deploy.rbとかproduction.rb、staging.rbのパスを変更できるのか気になったのでソースコードを読んでみました。
結論としては、変更可能ですがcapistrano/setupをCapfileでrequireする前にsetする必要があります。
また、Capfileファイルのパスも変えられるかが気になって見てみましたが、capfile、capfile.rb、Capfile.rbにファイル名の変更は可能という結論になりました。

/lib/capistrano/application.rb

色々省略してますが、初期化処理としてcapfileの読み込みを行います。

module Capistrano
  class Application < Rake::Application
    def initialize
      super
      @rakefiles = %w{capfile Capfile capfile.rb Capfile.rb} << capfile
    end

    private

    # allows the `cap install` task to load without a capfile
    def capfile
      File.expand_path(File.join(File.dirname(__FILE__),'..','Capfile'))
    end
  end
end

/lib/capistrano/setup.rb

load 'capistrano/defaults.rb'では、/lib/capistrano/defaults.rbで、setしなかった場合のデフォルト値が定義されています。
load:defaultsをImmutableTask(/lib/capistrano/immutable_task.rb)として定義しているので、CapistranoのタスクでもOverride出来ません。
stage_config_pathやdeploy_config_pathは/lib/capistrano/dsl/paths.rbで定義されています。
configure_backend は/lib/capistrano/configuration.rbで定義されていて、sshkitの設定を行います。

namespace :load do
  task :defaults do
    load 'capistrano/defaults.rb'
  end
end

stages.each do |stage|
  Rake::Task.define_task(stage) do
    set(:stage, stage.to_sym)

    invoke 'load:defaults'
    Rake.application["load:defaults"].extend(Capistrano::ImmutableTask)
    load deploy_config_path
    load stage_config_path.join("#{stage}.rb")
    load "capistrano/#{fetch(:scm)}.rb"
    I18n.locale = fetch(:locale, :en)
    configure_backend
  end
end

require 'capistrano/dotfile'

setup.rbの最後でrequireしているdotfile.rbの内容は以下のとおりです。

dotfile = Pathname.new(File.join(Dir.home, '.capfile'))
load dotfile if dotfile.file?

/lib/capistrano/install.rb

ファイルの中身は、以下のようになっています。

load File.expand_path(File.join(File.dirname(__FILE__),'tasks/install.rake'))

/lib/capistrano/task/install.rakeを実行しています。
install.rakeはcap install の際に実行される処理が定義されています。

/lib/capistrano/deploy.rb

require 'capistrano/framework'

load File.expand_path("../tasks/deploy.rake", __FILE__)

/lib/capistrano/framewok.rb

以下のように、framework.rakeを呼び出しています。

load File.expand_path("../tasks/framework.rake", __FILE__)
require 'capistrano/install'

task/deploy.rake

task/framework.rakeでは、Capistranoが提供するコマンド(?)のインターフェースが定義されています。

namespace :deploy do

  desc 'Start a deployment, make sure server(s) ready.'
  task :starting do
  end

  desc 'Started'
  task :started do
  end
 
  #以下、省略

また、:rollback、:deployの処理はここで定義されています。

  #以下、省略

  desc 'Rollback to previous release.'
  task :rollback do
    %w{ starting started
        reverting reverted
        publishing published
        finishing_rollback finished }.each do |task|
      invoke "deploy:#{task}"
    end
  end
end

desc 'Deploy a new release.'
task :deploy do
  set(:deploying, true)
  %w{ starting started
      updating updated
      publishing published
      finishing finished }.each do |task|
    invoke "deploy:#{task}"
  end
end
task default: :deploy

task/framework.rake

task/deploy.rake はtask/framework.rakeで定義してあったインターフェースの実装を担当しています。(全てではないですが)
また、実際の処理はinvokeで処理を委譲しています。

namespace :deploy do

  task :starting do
    invoke 'deploy:check'
    invoke 'deploy:set_previous_revision'
  end

  #以下、省略

Nginxのモジュールの作り方 C言語でHandlers編

概要

モジュールの種類

Nginxで開発できるmoduleの種類は、ざっくりとわけると以下の3種類があります。

  • Handlers・・・locationを指定して処理が可能
  • Load balancers・・・upstream moduleやload balancer moduleと連携して使う。load balancer moduleにはRound RobinとIP Hashがbuild-inされているけど、それ以外の分散方法として使うことが出来る
  • Filters・・・Handlerのレスポンスに対して処理を行う。例えばgzip filterはresponseをgzip圧縮する。

今回対象とするのはHandlersをC言語で実装することにします。

必要となるファイル

特定パスにアクセスしたら『hello world』と出力するようなモジュールを作ってみます。
stub_status moduleのイケてない版のようのモジュールです。
nginxのモジュールを実装するには、最低限2つのファイルが必要になります。

  • config
  • ngx_http_hello_module.c

configファイルにはモジュールの概要を書き、モジュール実態はngx_http_hello_module.cになります。

動作イメージ

configファイルのserverディテクティブの中に以下のように記載すると、http://localhost/hello/ にアクセスするとHello WorldをHTTP Responseとして返す。

location /hello/ {
   hello 'Hello World';
} 

実装

config

configファイルにはモジュールの概要や説明などを書きます。

ngx_addon_name=ngx_http_hello_module
HTTP_MODULES="$HTTP_MODULES ngx_http_hello_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_hello_module.c"

ngx_addon_nameにはモジュールの名前を書きます。
HTTP_MODULESには依存するモジュールの名前を書きます。
NGIX_ADDON_SRCSにはモジュールのパスを書きます。

HTTP_MODULESの他にどんなModuleがあるかは、auto/modulesのスクリプトを見るとわかります。
1.9.2では以下のように書かれています。

459     modules="$modules $HTTP_MODULES $HTTP_FILTER_MODULES \
460              $HTTP_HEADERS_FILTER_MODULE \
461              $HTTP_AUX_FILTER_MODULES \
462              $HTTP_COPY_FILTER_MODULE \
463              $HTTP_RANGE_BODY_FILTER_MODULE \
464              $HTTP_NOT_MODIFIED_FILTER_MODULE"

module.c

コンフィグファイルの定義

NginxのConfigファイルのどのディテクティブ(main, server, location)で、このモジュールの設定が定義出来るか決めます。
ngx_http<モジュール名>(main|srv|loc)_conf_t という構造体で、ディレクティブの設定値を保存する構造体を定義します。

その他のngx_http_core_loc_conf_tなどの定義は、src/core/ngx_config.h にある。
ngx_str_t のような型は、src/core/nginx_config.h にある。

typedef struct {
  ngx_str_t   name;
} ngx_http_hello_loc_conf_t;

ディテクティブに具体的にどんな内容を書くかは以下のように定義します。

static ngx_conf_post_handler_pt ngx_http_hello_p = ngx_http_hello;

static ngx_command_t ngx_http_hello_commands[] = {
  { 
    ngx_string("hello"),
    NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
    ngx_conf_set_str_slot,
    NGX_HTTP_LOC_CONF_OFFSET,
    offsetof(ngx_http_hello_loc_conf_t, name),
    &ngx_http_hello_p 
  },
  ngx_null_command
};

このモジュールを利用するには、コンフィグファイル内にhelloというKeyを定義するのが一番最初の『ngx_string("hello")』です。
次の『NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1』は、HTTPのLocationディテクティブで利用し、"hello"が引数を1つ取るということです。
引数の個数についてはsrc/core/ngx_conf_file.h に、コンフィグファイルのどのディテクティブに定義を表するかは src/http/ngx_http_config.h に定義されています。
3番目の『ngx_conf_set_str_slot』引数を取る場合の型を定義するお作法として、関数ポインタを記載します。ここで記載出来る関数名はsrc/core/ngx_conf_file.h の328行目から340行目までに定義されています。
4番目の 『NGX_HTTP_LOC_CONF_OFFSET』はLocationディテクティブの時にはこれを設定します。src/http/ngx_http_config.hの50〜52にsrv、mainの時に設定する関数が定義されています。
5番目では、ngx_http_hello_loc_conf_t->nameに設定
ngx_null_commandは、配列の最後を表します。モジュールとしてhello以外Keyが存在する場合は、ngx_null_commandの前に同様の定義をします。

module contextの設定

ngx_httpmodule_ctxという名前で、下記のコメントのようなコンフィグに対する処理を定義します。
ngx_http_hello_create_loc_conf関数で、コンフィグの初期化をしていますが、複数のlocationに対応するような場合にはngx_httpmerge_loc_confというような名前にする必要があります。

static ngx_http_module_t ngx_http_hello_module_ctx = {
  NULL,                          /* pre configuration */
  NULL,                          /* post configuration */
  NULL,                          /* create main configuration */
  NULL,                          /* init main configuration */
  NULL,                          /* create server configuration */
  NULL,                          /* merge server configuration */
  ngx_http_hello_create_loc_conf, /* create location configuration */
  NULL                           /* merge location configuration */
};

/* The function which initializes memory for the module configuration structure
*/
static void *ngx_http_hello_create_loc_conf(ngx_conf_t *cf)
{
  ngx_http_hello_loc_conf_t  *conf;
  conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_hello_loc_conf_t));
  if (conf == NULL) {
   return NULL;
  }
  return conf;
}

moduleの定義

NGX_MODULE_V1とNGX_MODULE_V1_PADDINGで挟んだ内容がモジュールの定義(ngx_module_t)になります。
それぞれの処理に合わせてコールバック処理登録を登録します。

/*
 * The module which binds the context and commands
 *
 */
ngx_module_t ngx_http_hello_module = {
  NGX_MODULE_V1,
     &ngx_http_hello_module_ctx,    /* module context */
     ngx_http_hello_commands,       /* module directives */
     NGX_HTTP_MODULE,               /* module type(CORE, MAIL, EVENT, ...) */
     NULL,                          /* init master */
     NULL,                          /* init module */
     NULL,                          /* init process */
     NULL,                          /* init thread */
     NULL,                          /* exit thread */
     NULL,                          /* exit process */
     NULL,                          /* exit master */
     NGX_MODULE_V1_PADDING
};

Handler

Handlerでは、リクエストを受けてレスポンスか返すという実質的な処理を実装します。

/*
 * Main handler function of the module.
 */
static ngx_int_t ngx_http_hello_handler(ngx_http_request_t *r)
{
  ngx_int_t    rc;
  ngx_buf_t   *b;
  ngx_chain_t  out;
  /* we response to 'GET' and 'HEAD' requests only */
  if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {
    return NGX_HTTP_NOT_ALLOWED;
  }

  /* discard request body, since we don't need it here */
  rc = ngx_http_discard_request_body(r);
  if (rc != NGX_OK) {
    return rc;
  }

  /* set the 'Content-type' header */
  r->headers_out.content_type_len = sizeof("text/html") - 1;
  r->headers_out.content_type.data = (u_char *) "text/html";
  /* send the header only, if the request type is http 'HEAD' */
  if (r->method == NGX_HTTP_HEAD) {
    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.content_length_n = hello_string.len;

    return ngx_http_send_header(r);
  }
  /* allocate a buffer for your response body */
  b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
  if (b == NULL) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
  }
  /* attach this buffer to the buffer chain */
  out.buf = b;
  out.next = NULL;

  /* adjust the pointers of the buffer */
  b->pos = hello_string.data;
  b->last = hello_string.data + hello_string.len;
  b->memory = 1;    /* this buffer is in memory */
  b->last_buf = 1;  /* this is the last buffer in the buffer chain */
  /* set the status line */
  r->headers_out.status = NGX_HTTP_OK;
  r->headers_out.content_length_n = hello_string.len;
  /* send the headers of your response */
  rc = ngx_http_send_header(r);
  if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
    return rc;
  }

  /* send the buffer chain of your response */
  return ngx_http_output_filter(r, &out);
}

ビルドしてみる

モジュールのソースコードを配備します。

~# mkdir /usr/local/src/ngx_http_hello_module
~# ls /usr/local/src/ngx_http_hello_module
config  ngx_http_hello_module.c

nginx-buildの0.4.0以降を使ってビルドします。

~# useradd --shell /sbin/nologin nginx
~# apt-get update
~# apt-get install gcc make
~# wget https://github.com/cubicdaiya/nginx-build/releases/download/v0.4.1/nginx-build-linux-amd64-0.4.1.tar.gz
~# tar xvfz nginx-build-linux-amd64-0.4.1.tar.gz
~# chmod +x nginx-build 
~# ./nginx-build -d work -pcre -zlib -openssl -verbose --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --pid-path=/var/run/nginx.pid --lock-path=/var/lock/nginx.lock --http-log-path=/var/log/nginx/access.log --with-http_stub_status_module --with-debug --add-module=/usr/local/src/ngx_http_hello_module
~# cd work/1.9.2/nginx-1.9.2/
~/work/1.9.2/nginx-1.9.2# make install
~#  /usr/sbin/nginx -c /etc/nginx/nginx.conf

ソースコードhideji/ngx_http_hello_module · GitHubにあります。

起動スクリプトを利用る場合はJasonGiedymin/nginx-init-ubuntu · GitHubを参照してください。

参考資料

簡単なHandlerの作り方は以下が参考になります。