読者です 読者をやめる 読者になる 読者になる

angular2でelasticsearchの生ログ検索ページを作る

angular2 elasticsearch

初めまして。アドテクdivのswfzです。

少し前に流行った fluentd + elasticsearch + kibanaでアクセスログを可視化、チームでもやっていましたが集計したグラフはみれるしめちゃいい!けど実際の生データを検索かけるのはちょっと操作が面倒ですよね。

f:id:AdwaysEngineerBlog:20170324155659p:plain

今回はangular2を使ってelasticsearchの生ログを検索できるSPAを作成してみました。

今回は作ったSPAの実際の動作と実装を軽く紹介させていただきます。

※データはダミーデータ生成用のgemがあるのでそれを使わせてもらいました。

動機

  • アクセスログの量が重いと調査でgrepとかする際時間がかかりストレスがマッハ(ノ`Д´)ノ.:・┻┻)
  • ログをelasticsearchに突っ込めばファイルから検索するより早く検索できる
    • 集計したグラフも見れて二度おいしい
    • ただ、検索クエリ覚えるのつらい
  • ディレクターや非エンジニアの方でもログ調査くらいなら出来るようになればお互いハッピー
    • エンジニアは作業を中断しなくて良く余計なコミュニケーションコストがなくなる
    • ディレクターはわざわざエンジニアに調査依頼せず自分で調査できる

elasticsearchにはRESTAPIがあるので直接リクエストを投げて問い合わせることで簡単にデータを検索することができます。

ということで、angular2を使ってelasticsearchのRESTAPIを叩くSPAを作ってみました。

angular2を使ったのは今後業務で使っていくので勉強し始めたからといった理由です。

ページ数が少なく簡単な作りになると思うのであえてangular2でやる必要はない気がしますが今回は気にしないことにします。

動作

それでは、実際の動作を見てみます。

各項目に対して検索条件を入力します。

f:id:AdwaysEngineerBlog:20170324155840p:plain

色々入力して検索すると

f:id:AdwaysEngineerBlog:20170324155849p:plain

こんな感じで結果が出てきます

クエリパラメータも確認することもあるなと思ったのでセルをクリックすることでクエリパラメータの一覧を見れるようにしました。

f:id:AdwaysEngineerBlog:20170324155906p:plain

大まかな流れはこんな感じです。

実装

次は実際に処理の流れを追っていきます。

angular-cliで雛形の作成

angular-cliを使ってアプリケーションの雛形を作成します。

  • インストール
npm install -g angular-cli rxjs
  • プロジェクトの作成
ng new ngapp --style=scss
cd ngapp
ng serve --host=0.0.0.0

これだけで動くものが作れてしまいます。簡単ですね。

  • 構成

下記のファイルたちが生成されました。

angular-cli.json
package.json
tslint.json
protractor.conf.js
src
|-- app
|   |-- app.component.html
|   |-- app.component.scss
|   |-- app.component.spec.ts
|   |-- app.component.ts
|   `-- app.module.ts
|-- assets
|-- environments
|   |-- environment.prod.ts
|   `-- environment.ts
|-- favicon.ico
|-- index.html
|-- main.ts
|-- polyfills.ts
|-- styles.scss
|-- test.ts
`-- tsconfig.json

ag-gridの導入

検索結果にgrid表示ができるライブラリを使用します。

有料版もあるのですが、無料の分だけでも十分使えると思います。

公式にサンプルが沢山あるので導入方法やグリッド部分の実装に関しては割愛させていただきます。

Javascript Datagrid

www.ag-grid.com

Angular Datagrid

www.ag-grid.com

コンポーネントの実装

今回は二つのタイプのログを検索できるようにしたかったので検索条件フォームのコンポーネントは二つ用意します

コンポーネント構成は図のようにしました

f:id:AdwaysEngineerBlog:20170324155934p:plain

実際のファイル構成は下記にしました

|-- components
|   | # 検索条件を入力するフォーム(access log)
|   |-- access-log
|   |   |-- access-log.component.html
|   |   |-- access-log.component.scss
|   |   |-- access-log.component.ts
|   |   `-- access-log.service.ts
|   |-- ag-grid
|   |   |-- ag-grid-cell
|   |   |   | # ag-gridのセル用(jsonデータ整形)
|   |   |   |-- ag-grid-cell-json-data
|   |   |   |   |-- ag-grid-cell-json-data.component.html
|   |   |   |   |-- ag-grid-cell-json-data.component.scss
|   |   |   |   `-- ag-grid-cell-json-data.component.ts
|   |   |   | # ag-gridのセル用(リクエストパラメータ整形)
|   |   |   |-- ag-grid-cell-search-params
|   |   |   |   |-- ag-grid-cell-search-params.component.html
|   |   |   |   |-- ag-grid-cell-search-params.component.scss
|   |   |   |   `-- ag-grid-cell-search-params.component.ts
|   |   |   `-- index.ts
|   |   | # 検索結果表示用
|   |   |-- ag-grid.component.html
|   |   |-- ag-grid.component.scss
|   |   `-- ag-grid.component.ts
|   | # 検索条件を入力するフォーム(twitter api log)
|   `-- twitter-api
|       |-- twitter-api.component.html
|       |-- twitter-api.component.scss
|       |-- twitter-api.component.ts
|       `-- twitter-api.service.ts

データバインド

検索条件フォームのコンポーネントから検索結果表示用のコンポーネントへ片方向のデータバインドを行っています。

まず子コンポーネントを呼び出す側から見ていきます。

  • src/app/components/access-log/access-log.component.html(検索条件フォーム)
<div class="container">
  <app-ag-grid [searchedData]="searchedData"
               [isSearchingToggle]="isSearchingToggle"
               [columnDefs]="columnDefs">
  </app-ag-grid>
</div>

app-ag-gridタグで子コンポーネントを呼び出します。

左辺が子コンポーネントで扱う値、右辺が親コンポーネントで持っている値です。

子コンポーネントは下記のようなコードになっています。先ほどのapp-ag-gridタグはselectorで定義された値です。

このselectorを他のhtmlで記述すればこのコンポーネントを呼び出すことが出来ます。

  • src/app/components/ag-grid/ag-grid.component.ts
import {Component, Input, OnChanges,AfterViewInit,SimpleChanges} from '@angular/core';
import {GridOptions} from "ag-grid";

@Component({
  selector: 'app-ag-grid',
  templateUrl: './ag-grid.component.html',
  styleUrls: ['./ag-grid.component.scss']
})
export class AgGridComponent implements OnChanges, AfterViewInit{
  @Input() searchedData: any;
  @Input() isSearchingToggle: boolean;
  @Input() columnDefs: any;

  private gridOptions: GridOptions;

  constructor(
  ) {
    this.gridOptions = <GridOptions>{
      enableSorting :true,
      enableFilter :true,
      enableColResize :true,
      rowHeight :50,
      enableCellChangeFlash :true
    };
    this.gridOptions.columnDefs = this.columnDefs;
    this.gridOptions.rowData = [];
  }

  ngAfterViewInit() {
    this.gridOptions.api.setColumnDefs(this.columnDefs);
  }

  ngOnChanges(changes: any) {
    if ( this.gridOptions.api ) {
      if ( changes.searchedData ) {
        this.gridOptions.api.hideOverlay();
        this.gridOptions.api.setRowData(this.searchedData);
      }

      if ( changes.isSearchingToggle ) {
        this.gridOptions.api.showLoadingOverlay();
      }
    }
  }
}

検索結果表示用のコンポーネントでは親コンポーネントから@Inputでデータを受け取ります。

受け取ったデータに変更があったらngOnChangesライフサイクルフックを用いてグリッドのデータを更新します。

また、初期のカラム定義this.columnDefsを親コンポーネントから受け取るようにしたためag-gridのapiを使ってカラム定義を更新する必要があります。

しかしag-gridのapithis.gridOptions.apiがviewを生成してからでないと使うことが出来ないためngAfterViewInitライフサイクルフックを用いてビューの生成後にカラム定義を行うようにしています。

またthis.columnDefsの定義次第でag-gridの各セルに対してもコンポーネントを用意することで自由に表現することが出来ますが、ここでは説明を割愛します。

elasticsearchへのクエリ組み立て

elasticsearchへのリクエストを送る部分は共通で使えるのでserviceディレクトリを作成しそこに実装します。

このファイルではelasticsearchへクエリを投げる処理のみを行います。

`-- services
    `-- es-search.service.ts

実際のクエリの中身は呼び出し元の親コンポーネントのディレクトリでservice.tsを作成しそこで対象のログに合わせた関数を書きます。

  • src/app/components/access-log/access-log.service.ts
.....
.....
.....
  buildRequestBody(params: any): any {
    let bodyParams = { "size": params.size };

    let paramsCount = Object.keys(params).filter(k => params[k].length > 0).length;
    if (paramsCount > 1) {
      bodyParams["query"] = {"bool": {"filter": []}};
    }

    if (params.code) {
      if (params.not_code){
        bodyParams["query"]["bool"]["filter"].push(
          {
            "bool": {
              "must_not": {
                "terms": {
                  "code": params.code.split(',')
                }
              }
            }
          }
        )
      }else{
        bodyParams["query"]["bool"]["filter"].push(
          {
            "terms":
              { "code": params.code.split(',') }
          }
        );
      }
    }
.....
.....
.....
    return bodyParams;
  }

boolクエリやfilterクエリを用いて複数の条件を入力してもよしなに検索してくれるようにjsonを組み立てます。

多分この作業が一番大変な気がします…

検索処理

最後に検索ボタンを押した際に呼ばれる関数searchを実装します。

accessLogServiceからリクエストに必要なパラメータを取得してきてesSearchServiceでelasticsearchへクエリを投げます。

返ってきたデータをインスタンス変数に格納し、view側で表示させます。

データバインドの項で紹介したように、検索結果表示用のコンポーネントにデータバインドしているので親コンポーネントのデータが変わったらその都度子コンポーネント側で更新してくれるような実装になっています。

  • src/app/components/access-log/access-log.component.ts
.....
.....
.....
  private columnDefs: any;
  private searchedData: any = [];
  private totalCount: number;
  private displayCount: number;
  private isSearchingToggle: boolean = false;

  constructor(
    private esSearchService: EsSearchService,
    private accessLogService: AccessLogService
  ) { }

  ngOnInit() {
    this.columnDefs = this.accessLogService.columnDefs;
  }

  search(params: any): void {
    this.isSearchingToggle =  ( this.isSearchingToggle ) ? false : true;

    let pathName  = this.accessLogService.getPath(params);
    let jsonQuery = this.accessLogService.buildRequestBody(params);
    this.esSearchService.search(pathName,jsonQuery).subscribe(
      data => {
        this.searchedData = data.hits.hits.map(row => row._source);
        this.totalCount   = data.hits.total;
        this.displayCount = data.hits.hits.length;
      },
      error => {
        console.log('search error');
      }
    );
  }
}

以上の流れでelasticsearchへクエリを投げてデータを表示するといった処理を実装することが出来ました。

まとめ

初めてのangular2ということで、入門書片手にangular2を触ってみました。

今までフロントエンドに対して避けてきた部分があったのですが、angular2はてとても楽しく実装を進めることができました。

感覚的にですが、サーバサイドのコードを書く感覚で書くことができるからかなと思います。

また機会があれば何か書きたいと思います。

下記サンプルです

swfz/angular2-es-raw-search: raw log search from Elasticsearch

github.com

ちょっとRubyの中を覗いてみよう

Ruby

久保田です。

ある程度プログラムを書いていると、ふと、これはなぜ動いているんだろう。。。ただの文字列じゃないか。。。と思うことがあると思います。

というわけで今回はちょっとだけ、Rubyがどのように動いているかを覗き見してみます。
(Rubyがどういった規則で構文解析をしているかなどには触れません。あくまでYARVに渡されるまでの流れを追っていきます。)

例えば以下のようなRubyのプログラムです。

  • foo.rb
result = 1 + 2
puts result

誰でも3が出力されるとわかるようなプログラムですが、$ ruby foo.rb と動かし、3を出力する間に何が起きているでしょう。

実はRubyでは、渡されたプログラムをそのまま実行しているのではなく、 Rubyというプログラムが理解できる形に変換してから実行しています。

そして変換は、字句解析 -> 構文解析 -> コンパイル という3つのステップを経て変換されていきます。

今回はこの3つのステップを少しずつ追っていきたいと思います。

字句解析

まずは字句解析です。
字句解析はただの文字列であるプログラムを トークン という意味のある単語に分ける仕事をします。
foo.rbの一行目でいうと、
resultという変数 / = という演算子 / 1という数字 / +という演算子 / 2という数字 に分けていきます。
(空白や改行などもトークンとして認識しますが、一旦無視します。)

それでは、実際にfoo.rbを字句解析してみましょう。
Rubyには標準ライブラリとしてRipperというものがあり、Ripperを使うと字句解析や構文解析の結果を渡してくれます。

require 'ripper'
require 'pp'

code =<<EOF
result = 1 + 2
puts result
EOF

pp Ripper.lex(code)

結果

[[[1, 0], :on_ident, "result"],
 [[1, 6], :on_sp, " "],
 [[1, 7], :on_op, "="],
 [[1, 8], :on_sp, " "],
 [[1, 9], :on_int, "1"],
 [[1, 10], :on_sp, " "],
 [[1, 11], :on_op, "+"],
 [[1, 12], :on_sp, " "],
 [[1, 13], :on_int, "2"],
 [[1, 14], :on_nl, "\n"],
 [[2, 0], :on_ident, "puts"],
 [[2, 4], :on_sp, " "],
 [[2, 5], :on_ident, "result"],
 [[2, 11], :on_nl, "\n"]]

配列の入れ子でプログラムが トークン に分けられていますね。
一つ目の配列を見て、構成を見てみます。

[[1, 0], :on_ident, "result"]

配列の最小の要素に、プログラム中の[何行目, 何文字目]かが格納されています。
次に、このトークンの種類が入っています。
最後の要素には実際に使われている文字列が入っています。

字句解析はこのように行なわれて、意味のある文字列に分けられているようですね。
ただこの状態では、まだ文としては意味を持っていません。
なので、字句解析の後、Rubyはこの分けられたトークンを元に構文解析を行います。

構文解析

構文解析では、意味のある文字に分けられたRubyプログラムを意味のある文にグループ化し、ASTノードにしていきます。つまり、 パース をします。
パースは、パーサジェネレータを使って行います。Rubyではbisonを使ってパーサジェネレータを作成します。

早速どのような動作をするか確認してみます。

require 'ripper'
require 'pp'

code =<<EOF
result = 1 + 2
puts result
EOF

pp Ripper.sexp(code)

先ほどの字句解析のRipper.lexをRipper.sexpに変更しただけです。

結果

[:program,
 [[:assign,
   [:var_field, [:@ident, "result", [1, 0]]],
   [:binary, [:@int, "1", [1, 9]], :+, [:@int, "2", [1, 13]]]],
  [:command,
   [:@ident, "puts", [2, 0]],
   [:args_add_block, [[:var_ref, [:@ident, "result", [2, 5]]]], false]]]]

先程よりはちょっと複雑な入れ子の配列になっています。
これがASTノードと呼ばれる構造です。
一部を抜き出すと、こういった構造ですね。

f:id:AdwaysEngineerBlog:20170317135428p:plain

少しだけ見ていきます。
まず、配列の最初の要素に:programと書かれています。始まるよー!的な意味ですね。
そして同じ階層のもう一つの要素は配列になっており、その配列の要素は二つありどちらも配列です。
その配列の構造も[シンボル: [配列]]となっているようです。

ここではASTに関しては詳しくは述べませんが、
それぞれの配列の最初のシンボルになっている要素が グループ化された文の種類 を表し、後に続く要素が配列の場合はさらにグループ化されています。 そして続く要素が配列でなくなった時に解析が終了しています。

このように 意味のある文字列(トークン) 意味のある文(ASTノード) にしています。
ここまで来てやっと、Rubyはどのようにただの文字列であるプログラムをどのように扱っていいかがわかるというわけです。

実は、このRuby1.9以前はこの状態まででプログラムの変換は終了だったそうです。
しかしこのままでは実行速度に問題あったらしく、Ruby1.9から笹田耕一さんにより開発されたYARVというRubyの仮想マシンが組みこまれました。

そのYARVがプログラムを実行するため、先ほどの構文木をコンパイルし、YARVが実行できる形式に変換します。

コンパイル

3つのステップの最後、コンパイルです。
早速やってみましょう。

code =<<EOF
result = 1 + 2
puts result
EOF

puts RubyVM::InstructionSequence.compile(code).disasm

結果

== disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, keyword: 0@3] s1)
[ 2] result
0000 trace            1                                               (   1)
0002 putobject_OP_INT2FIX_O_1_C_
0003 putobject        2
0005 opt_plus         <callinfo!mid:+, argc:1, ARGS_SKIP>
0007 setlocal_OP__WC__0 2
0009 trace            1                                               (   2)
0011 putself
0012 getlocal_OP__WC__0 2
0014 opt_send_simple  <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
0016 leave

さぁ、ついに意味がわからなくなってきました。笑
正直YARVの処理はまだあまり理解していません。。。
YARVの話だけで一ヶ月ブログ書けるくらいのボリュームだと思っています 。。

かろうじてわかるのは、
0007 setlocal_OP__WC__0 2
で変数をセットして、
0012 getlocal_OP__WC__0 2
で呼び出し、
0014 opt_send_simple <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
でputsメソッドを呼び出しているのだろう、ということくらいですね。笑

数字の後に続いているのがYARV命令というやつで、YARVが何をするかはここで決めています。

ここら辺の話はまた今度できたらな、と思います。

まとめ

Rubyは、プログラムを渡されると実行までに上記の3ステップの長〜い道のりを行くんですね。
いつも何気なく動かしているプログラムですが、ものすごく高度な技術が折り重なってできているんですね。
皆さんも是非自分のプログラムを覗き見してみてください。

参考文献

CUIで動くプログレスバーを作ろう

久保田です。

今日は小ネタです。

エンジニアの方々は、おそらく毎日コマンドラインを使っていると思います。
そして、コマンドラインを使っているなら、このような表示をwgetやdockerなど様々なところで見かけると思います。

f:id:AdwaysEngineerBlog:20170310170213g:plain

僕はエンジニアになりたての頃、このプログレスバーがどのように作られているかふわっと疑問に思っていました。
普通にコンソールにログを出したらこうはならないじゃないか、と
一行ずつ表示しちゃうじゃないか、と

でもある日 キャリッジリターン だと気がつきました。
通常改行時に\nとセットで使う\r、これを使います。

\rだけで使用すると、カーソル行頭復帰するので、
出力 -> キャリッジリターン -> 出力 -> キャリッジリターン -> 出力 を繰り返せばよかったのです。

歴史的には、以下のような感じらしいです。(wikipediaより)

キャリッジ・リターン(carriage return)は本来、テレタイプ端末の Baudot Code における制御文字を指す用語で、行末から行頭に戻す復帰コードであって、改行コードを含まない。その後、タイプライターで一行打ち込んだ後で紙を固定するシリンダー(キャリッジ)を次の行の先頭にタイプできるように戻し(リターン)改行する機構(またはその機構を操作するレバー)を「キャリッジ・リターン」と呼んだ。

というわけで、goで実装しました。

今回は例なので、
- 100%になるまでランダムに計算を繰り返し、%に合うだけ=を出力 & キャリッジリターンする。
- 今回はランダムで数字を与え続け、30になるまで繰り返す。
という流れです。

package main

import (
    "fmt"
    "math/rand"
    "strings"
    "time"
)

type ProgressBar struct {
    max         int
    progress int
    percent   float64
}

func main() {
        // 乱数生成器作成
    rand.Seed(time.Now().UnixNano())
        // プログレスバーを表す構造体
    progressBar := ProgressBar{max: 30, progress: 0}

    for {
        // 加算処理
        randNum := rand.Intn(10)
        progressBar.progress += int(randNum)

         // max値を超えないようにする
        if progressBar.progress > progressBar.max {
            progressBar.progress -= (progressBar.progress - progressBar.max)
        }

        // プログレスのパーセンテージ
        progressBar.percent = (float64(progressBar.progress) / float64(progressBar.max)) * 100
                // いくつ=を表示するかを決めるため、intに変換
        roundPercent := int(progressBar.percent)
                // ===> のように >付きで表示したいので、-1する。
        if roundPercent > 0 {
            roundPercent -= 1
        }
     // .も合わせて最大5桁、小数点は1桁で出力。
        fmt.Print(fmt.Sprintf("%5.1f%% [%s>%s] %d \r", progressBar.percent, strings.Repeat("=", roundPercent), strings.Repeat(" ", 99-roundPercent), progressBar.progress))

        if progressBar.progress == progressBar.max {
            fmt.Println("")
            break
        }

        time.Sleep(1 * time.Second)
    }
}

こんな感じです。

f:id:AdwaysEngineerBlog:20170310170033g:plain

今回は簡単な例ですが、 このような出力の仕組みを利用すると、CUIで人を驚かせられるような面白い遊びができるので、ぜひ試してみてください。
(表示上は消えているように見せたり、動いているように見せたりできます。)

mackerel-agent-pluginsにコントリビュートした話

久保田です。

最近、mackerel-agent-pluginsにプラグインを作り、コントリビュートしました。

人生初のossへのコントリビュートだったので、その話を忘れないうちに書いておきます。

今回、僕はGoogle Cloud PlatformのCompute Engineに入れることでcustom metric を収集することのできるプラグインを書きました。

すでに存在していた、mackerel-plugin-aws-ec2のGCPバージョン、という位置付けで作りました。

僕がgcpを使っているということは正直なく、ただOSSに貢献したかったから作った、というものになります。

そして無事マージしていただきました。mackerel-agent-pluginsの方々、ありがとうございます。mm

github.com

というわけでマージまで至った流れと反省をつらつらと書いていきたいと思います。

大まかな流れ

マージまでの流れは以下のような感じでした。

  • 実装
  • READMEを書く
  • CIでテスト
  • プルリク
  • レビューしていただく
  • マージ

実装

今回、goで何かを作るのも初めてだったため、実装の前にいろいろな方のpluginを参考にしました。

mackerel-agent-pluginを作る時は、だいたい以下のような構成になります。 (mackerel-agent-plugin-gcp-compute-engineの場合)

|----- README.md
|----- main.go
|----- lib
     |------ gcp-compute-engine.go
     |------ gcp-compute-engine_test.go

かなり端折っていますが、パッケージの実装の構成は以下のような感じです。

  • gcp-compute-engine.go
package gcpce // パッケージ名を決める

import (
  mp "github.com/mackerelio/go-mackerel-plugin-helper"
   // importするパッケージを指定
)


// プラグインで使用する構造体。
// この構造体に必要なメソッドを作っていく。
type ComputeEnginePlugin struct {
    Project           string
    InstanceName      string
    MonitoringService *monitoring.Service
    Option            *Option
    Tempfile          string
}

// graphの定義をmap[string]に入れていく。
// それぞれが1つのグラフになる。
var graphdef = map[string]mp.Graphs{
    "Firewall.DroppedBytesCount": mp.Graphs{
        Label: "FireWall Dropped Bytes Count",
        Unit:  "bytes",  // 型の指定
        Metrics: []mp.Metrics{
            {Name: "dropped_bytes_count", Label: "Dropped Bytes Count", Type: "uint64"},
        },
    },
    "Firewall.DroppedPacketsCount": mp.Graphs{
        Label: "FireWall Dropped Packets Count",
        Unit:  "integer",
        Metrics: []mp.Metrics{
            {Name: "dropped_packets_count", Label: "Dropped Packets Count", Type: "uint64"},
        },
    }, 

        ....
}

// グラフの定義を返す構造体のメソッド。
// グラフを動的に変化させたい場合はここで処理する。
func (p ComputeEnginePlugin) GraphDefinition() map[string]mp.Graphs {
    return graphdef
}

// 最新の値を取得する関数。
// gcp-compute-engineの場合は、gcpのAPIコールをして値を取得する。
func getLatestValue() (interface{}, error) {
       ....
}

// mackerelに送信する値を取得し、まとめるメソッド。
// getLatestValueを必要に応じて呼び出し、メトリックを集める。
func (p ComputeEnginePlugin) FetchMetrics() (map[string]interface{}, error) {
       ....
}

// pluginのメイン処理。
func Do() {
    // コマンドライン引数を処理したり、メタデータを集める。
        ....

    var computeEngine = ComputeEnginePlugin{
        MonitoringService: service,
        Project:           "projects/" + projectID,
        InstanceName:      instanceName,
        Option:            &Option{Key: *optAPIKey},
    }

    // 構造体をmackerel-agent-plugin-helperに渡す。
    helper := mp.NewMackerelPlugin(computeEngine)

    if *optTempfile != "" {
        helper.Tempfile = *optTempfile
    } else {
        helper.Tempfile = fmt.Sprintf("/tmp/mackerel-plugin-gcp-compute-engine-%s", computeEngine.InstanceName)
    }


    helper.Run() // 渡した構造体のFetchMetricsメソッドなどを実行してくれる。 https://github.com/mackerelio/go-mackerel-plugin-helper/blob/master/mackerel-plugin.go#L310

}

次に最終的に動かすことになるmain.goです。 - main.go

package main

import (
    "github.com/mackerelio/mackerel-agent-plugins/mackerel-plugin-gcp-compute-engine/lib" // パッケージ本体をインポート
)

func main() {
    gcpce.Do()
}

プラグインで使用し、makerel-agent-plugin-helperに渡す構造体を作り、
graphdefにグラフ定義を書き、
グラフを返すGraphDefinitionメソッドを定義し、
値を収集するFetchMetricsメソッドを定義し、
Do関数で引数を処理したりし、構造体をmackerel-agent-plugin-helperに渡して、helper.Run()で実行

という流れです。

その他必要に応じてメソッドや構造体を実装します。

READMEを書く

READMEを書く、と言うと簡単じゃん、と思うかもしれません。僕もそう思っていました。
しかし実際書くとREADMEがシステムの行く末を決める、と言っても過言ではないなと思いました。

使う人にとってはREADMEの情報がほぼ全てだと思いますし、
(ほとんどの人は使い方がわからないときは、ソースの中まで見ずに他の解決法を探すと思います。)
よって、ここにどれだけわかりやすく簡潔に書くか、ということが求められると感じました。
システムのインターフェイス、と言ってしまってもいいかもしれません。

そして、このREADMEを英語で書かなければなりません。。。
割と読める方だとは思っていたのですが、書くとなるとどうしていいかわからないものでした。

なので、僕はちょくちょくブログも書いてくれる後輩であるアメリカ帰りの湯浅君に英語を教わりながらREADMEを書いていきました。
大感謝です。湯浅君がすごくカッコよく見えました。

もしかしたらREADMEが今回一番苦戦し、一番勉強になった箇所かもしれません。。。笑

CIでテスト

mackerel-agent-pluginsの場合は、

https://github.com/mackerelio/mackerel-agent-plugins/blob/master/CONTRIBUTING.md

こちらにコントリビュートへの手順がありました。
こちらに書いてある通り、CIでテストをパスする必要があります。
travisCIにforkしたリポジトリを登録し、 テストをします。

僕の場合は、golintでかなりテストを失敗していました。。。
goのお作法を知らなかったので、なかなか苦戦しました。

プルリク

CIでテストが通ったら、プルリクです。
人生初のプルリクということでかなりドキドキしていました。

この時、プルリクに残すメッセージに何が適切かわからず、今も何が適切かわかりません。。。
次は他の方のを参考にしたいと思います。

レビューをしていただく

ありがたいことにレビューをしていただきました。

astjさん、ありがとうございました。

僕自身goを書くのが初だったので、拙いコードを読ませてしまうことになり、大変心苦しかったです。。。

次はもっと綺麗なコードでプルリクできるようにします。
(レビューしていただいた後にタイポでテストが通らない、などしょうもないミスがありました。大変申し訳ありません。。。)

マージ

レビューしていただき、直し、テストも通したらあとは待つのみ、です。

僕は幸せなことにマージしていただいたので、現在は晴れてmackerel-agent-pluginsの一員になっています。

初のプルリク、ということで、反省がかなり多くなってしまいました。
どう使われるのが一番なのか、英語でREADMEを書くことなど、しかし普段の開発ではわからないことや初めてのことも多く、大変勉強になりました。

次はもっとスムーズにできるようにしたいなと思います。

改めまして、mackerelioの方々、ありがとうございました。

canvasタグを使って動画のスクリーンショットを撮る方法

html5

はじめまして。エンジニアブログ初投稿の南です。
広告代理店であるアドウェイズではさまざまな動画広告を媒体に配信しています。
ある日、「動画からサムネイルを手軽に作る機能がほしい」と要望がありました。
用件としては、

  • 動画広告はサムネイルによって効果が変わる場合があるので、一つの動画から複数のサムネイルを作成する場合がある
  • 毎回スクリーンショットを撮るのは面倒
  • 再生しながらボタン一つで作成したい

以上のようなものでした。
今回はhtml5で新たに追加されたcanvasタグとjavascriptを用いて実装してみようと思います。

html用意

動画を描画するhtmlを用意します。

<html>
  <head>
    <meta charset="utf-8">
    <script src="http://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
  </head>
  <body>
    <video id="video" src="test.mp4" controls></video>
    <button id="capture">キャプチャー</button>
    <a href="" id="download" download="test.png">ダウンロード</a>
    <canvas id="canvas"></canvas>
  </body>
</html>

javascriptでcanvasに描画

ボタンがクリックされた際の動画の状態をcanvasに描画します

$('#capture').on('click', function(){
  var video = document.getElementById('video');
  var $canvas = $('#canvas');
  
  $canvas.attr('width', video.videoWidth);
  $canvas.attr('height', video.videoHeight);
  $canvas[0].getContext('2d').drawImage(video, 0, 0, $canvas.width(), $canvas.height());
});

canvasにはwidthとheightをvideoタグと同じ大きさで設定しています。
canvasのサイズはデフォルトでは300px×150pxが設定されており、cssを用いてwidthなどを変更してしまうと、デフォルトサイズが適用され思うように描画することができません。

後はaタグのhref属性にcanvasのバイナリを埋め込む処理をon click処理内に追記すればダウンロードまで完成です。

$('#download').attr('href', $canvas[0].toDataURL('image/png'));

デモ

f:id:AdwaysEngineerBlog:20170224130140g:plain

よさげですね。

感想

それほど複雑なコードを書くことなく作成できたので、「canvasすげー」ってなりました。
業務ではRailsを利用しているので、バイナリをサーバに送信し、RMagickを使ってごにょごにょやってます。
そこら辺も機会があれば書きたいと思います。