構造化ログのススメとRuby向けロガーOugaiを作った理由

構造化ログ 構造化ログ とは、機械的に処理しやすいログのことであり、その機構(ロギング)である。 英語圏では、 Structured Logging と表記される。たとえば Google Cloud の Stackdriver のドキュメントには下記の説明ページがあります。(残念ながら執筆時点で、これの日本語ページがまだできてないので、Google がどう訳すか興味深い) Structured Logging | Stackdriver Logging | Google Cloud 普通のログと構造化ログの比較 普通のログは、基本的に タイムスタンプ 、レベル 、そして メッセージ の文字列だけである。ログとして残す事象(イベント)のコンテキストになる情報はメッセージに適当に埋め込む。コンソール等で人が読みやすいものである。 構造化ログは、メッセージに埋め込んでいたコンテキストになる情報をそれぞれログ構造のフィールドに独立して持たせる。そのため後から解析がしやすい。そして出力するログはテキストベースで JSON にすることが多い。 では、見比べてみましょう。通常のRubyのLoggerと自作のOugaiでのログは次のようになります。『ユーザが記事を作成した』というログです(冠詞、削ってます)。 logger.info "User created article (user_id=#{user.id} article_id=#{article.id}" I, [2018-05-13T17:51:08.772542 #6253] INFO -- : User created article (user_id=123 article_id=45) logger.info "User created article", user_id: user.id, article_id: article.id 1 {"pid":6253,"level":20,"time":"2018-05-13T17:52:25.147+09:00","msg":"User created article","user_id":123,"article_id":45} ※デフォルトフォーマットとは異なります 見てわかる通り、普通のログは埋め込んで付帯情報を文字列化しつつ書かなくてはなりません。一方、構造化ログの方が JSON にした影響で長くなるのでコンソールでは読みづらいです。しかし読みづらいことはフォーマッタの動作環境での切替やparse機構を持つログビューワを使えば問題になりません。 構造化ログの方が解析しやすいというのは、例えば普通のログでは「あるユーザのログだけ抽出したいとき」に単に grep "user_id=10" とすると user_id が 101 など他のものまで引っかけてしまいます。構造化ログでは(主に JSONPath を使って)フィルタが $.user_id = 10 のように簡単に確実に絞り込めます。 ...

2018年5月13日 · Toshimitsu Takahashi

Transgate という Node.js 製エージェントベースのタスクフローフレームワーク

Transgate というNode.js製のエージェントベースのタスクフローフレームワークを作った。 どうして作ったのか? 自宅の家電を操作するためのプログラムを書いていたら、色々なフローがごちゃごちゃになったから。Dysonのファンから定期的に温度湿度を取得してデータベースに保存したり、Google Home/Assistant + IFTTT から来るメッセージを処理してIRKit を操作する。そのうち温度に従って自動的に IRKit 経由でエアコンを操作したくもなった、さてどう書こうかと? どんなもの? 突然だけど空港などの荷物の仕分けをイメージしてください。エージェントは、ゲートから出てくるアイテムを受け取り、処理して別のゲートに送る。ゲートの向こう側がどうなっているかは、エージェントは何も知らない。エージェントは空のアイテムを来たら作業を終える。アーキテクチャのイメージはこんな感じです。 エージェントはゲートからアイテムを受け取ることと、別のゲートに新たにアイテムを送ることができる。アイテムはシンプルなオブジェクトだ。エージェントは自身のタスクに集中できる。だから前工程や次工程が増えても減ってもアイテムの構成が変わらなければ問題なく動く。そして入出力がシンプルなためユニットテストも簡単にかける。エージェントはゲートの実体を知らないので、入力元ゲートをスタブに、出力先ゲートをモックに、簡単に置き換えられる。 フレームワークに出てくる概念のまとめ ゲート(Gate)はファイルストレージやデータベース、キュー、APIサービスといった入出力のエンドポイントです。 エージェント(Agent)はゲート間でアイテムを処理するタスクワーカーです。ゲートが何に通じているかは関知しません。 アイテム(Item)はゲート間を流れるシンプルなオブジェクトでエージェントが処理する対象です。 使用例 今回のフレームワークを作るきっかけになったホームコントロールプログラムを通じて説明してみます。 ちなみにこのプログラムは靴箱の中の Raspberry PI 上でデーモンとして動いています。 構成図 メインプログラム (main.js) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 const { Agent, HttpClientGate, HttpServerGate, IntervalGate, JointGate, StdoutGate, duplicator, mixer, } = require('transgate'); const pino = require('pino')(); const config = require('konfig-yaml')(); const MongoGate = require('./lib/mongo_gate'); const IRKitGate = require('./lib/irkit_gate'); // Agent const AnalysisCommander = require('./lib/analysis_commander'); const DysonCoolLinkRecorder = require('./lib/dyson/cool_link_recorder'); const EnvironmentalAnalyzer = require('./lib/environmental_analyzer'); // Gate const slackGate = new HttpClientGate({ endpoint: config.slack.webhook_url }); const iftttGate = new HttpServerGate({ port: config.port }); const irkitGate = new IRKitGate(config.irkit.endpoint); const intervalGate = new IntervalGate(60); const mongoGate = new MongoGate(config.mongodb.endpoint, config.mongodb.collection); const drToEaGate = new JointGate(); (async () => { try { await Agent.all( new AnalysisCommander(iftttGate, { irkitGate, slackGate }), new DysonCoolLinkRecorder(intervalGate, duplicator(mongoGate, drToEaGate)), new EnvironmentalAnalyzer(drToEaGate, { irkitGate, slackGate }), ); } catch(err) { pino.error(err); await iftttGate.close(); await mongoGate.close(); } intervalGate.clear(); })() .catch(err => { pino.error(err); }); 7つのゲート slackGate は slack にテキストメッセージをポストします。HttpClientGate のインスタンスで、アイテムとなるJSON は { "text": "<text message>" } です。 iftttGate は IFTTT の webhook から受け取った JSON をアイテムとして利用します。アイテムとなるJSON は { "target": "TV", "text": "<speaking words>" } です。 irkitGate はHTTPインターフェイスを備える赤外線送信器に命令します。アイテムとなるJSON は { "command": "celling_light_off" } です。 intervalGate は一定の間隔でアイテムを生成します。アイテムは { "time": <Date instance> } です。この場合は 1 分おきにエージェントの処理を走らせます。 mongoGate は MongoDB の指定のコレクションに送信されたアイテムを登録します。 drToEaGate は後述の DysonCoolLinkRecorder から EnvironmentalAnalyzer にアイテムの流すジョイントです。 3つのエージェント AnalysisCommander は IFTTT の webhook から来た JSON をアイテムとして受け取り、操作対象とテキストから IRKit に対して送信すべき赤外線信号を指定します。slack には文言が解釈できなかったときにポストします。 DysonCoolLinkRecorder は Dyson PureCoolLink ファンから1分おきに温度と湿度を取得して、duplicator という複製機を挟んで MongoDB への書き込みとジョイントとなるゲートに送ります。 EnvironmentalAnalyzer はそのジョイントを通じて来た温度から閾値を超えていたらエアコンの操作を IRKit に要求します。自動的に操作をしたときは slack に記録します。 エージェントの実装 Agentのサブクラスを作ります。main メソッドで受け取ったアイテムを処理して指定先のゲートに新たなアイテムを送る処理を書きます。before/after のフックメソッドを使って、初期化処理や別に利用するプロセス(例えば headless chrome) をここで制御(起動・停止)します。 ...

2017年11月23日 · Toshimitsu Takahashi

Windows 上に Python 環境を構築して Tensorflow GPU + Keras で日本古典籍字形の文字認識を試すまで

tilfin.hatenablog.com こちらの記事で書いたように Windows で Tensorflow を GPU で試してみました。この中で Python をオフィシャルのインストーラから入れて virtualenv で動作環境を作成したのですが、後々 matplotlib や OpenCV を入れようとしたところ Windows のために諸々嵌ることがあったので、より簡単に All-in-One 導入できる Anaconda から入れてみることにしました。 www.procrasist.com またこちらの記事で見た Keras が、今後 Tensorflow で色々と試す上で非常分かりやすそうだったので、導入してみることにしました。 Anaconda で Python 3.5 をセットアップ 既にインストールしていた Python は「プログラムと機能」からアンインストールしておきました。 Download Anaconda Now! | Continuum から [Python 3.5 version 64-BIT INSTALLER] をダウンロードして実行します。インストール ウィザードで環境変数を通しておきます。(デフォルトのインストール場所ではなく D:\Anaconda35 に自分は入れました。) Tensorflow + Keras をセットアップ 環境構築 ここから先は PowerShell で行います。 conda で work という名前の環境を作ります。 PS D:\> conda create -n work python=3.5 anaconda …省略… Extracting packages ... [ COMPLETE ]|##################################################| 100% Linking packages ... 1 個のファイルをコピーしました。################## | 71% [ COMPLETE ]|##################################################| 100% # # To activate this environment, use: # > activate work # # To deactivate this environment, use: # > deactivate work # # * for power-users using bash, you must source # Tensorflow の導入 work 環境にして pip で tensorflow と tensorflow-GPU をインストールします。 ...

2017年1月8日 · Toshimitsu Takahashi

Ubuntu に OpenCV をインストールして Python で画像から顔をクロップするまで

以前に Tensorflow のセットアップをして、付属するサンプルを試すところまでして終わっていましたが、 実際に何か試してみないとということで、よくある顔判別をやるためにデータを用意する仕組みを作ろうとしてます。 Python 3 と OpenCV 3 を使って画像から顔検出してそれを正方形でクロップして同一サイズにするというスクリプトを作ります。 セットアップ ほぼ下記のサイトの通りやったところうまくいきました。 www.pyimagesearch.com 以前にcudaのライブラリを VirtualBox 上の仮想環境にに入れていたせいでコンパイルエラーになりましたが、 それも下記のオプションを指定して進められました。 codeyarns.com スクリプト Python はほとんど書いたことないのですが、色々と調べてなんとかそれらしいものができました。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import cv2 import sys import os class FaceCropper(object): CASCADE_PATH = "data/haarcascades/haarcascade_frontalface_default.xml" def __init__(self): self.face_cascade = cv2.CascadeClassifier(self.CASCADE_PATH) def generate(self, image_path, show_result): img = cv2.imread(image_path) if (img is None): print("Can't open image file") return 0 #img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = self.face_cascade.detectMultiScale(img, 1.1, 3, minSize=(100, 100)) if (faces is None): print('Failed to detect face') return 0 if (show_result): for (x, y, w, h) in faces: cv2.rectangle(img, (x,y), (x+w, y+h), (255,0,0), 2) cv2.imshow('img', img) cv2.waitKey(0) cv2.destroyAllWindows() facecnt = len(faces) print("Detected faces: %d" % facecnt) i = 0 #height, width = img.shape[:2] for (x, y, w, h) in faces: r = max(w, h) / 2 centerx = x + w / 2 centery = y + h / 2 nx = int(centerx - r) ny = int(centery - r) nr = int(r * 2) faceimg = img[ny:ny+nr, nx:nx+nr] lastimg = cv2.resize(faceimg, (32, 32)) i += 1 cv2.imwrite("image%d.jpg" % i, lastimg) if __name__ == '__main__': args = sys.argv argc = len(args) if (argc != 2): print('Usage: %s [image file]' % args[0]) quit() detecter = FaceCropper() detecter.generate(args[1], True) CASCADE_PATH は OpenCV に付属している顔検出の XML 定義のファイルパスを指定します。 顔検出は face_cascade.detectMultiScale の部分で最低の検出サイズを 100x100 として、scaleFactor=1.1, minNeighbors=3 を色々試行して良さそうな値としました。 また下記のサイトを参考にさせてもらいました。 物体検出(detectMultiScale)をパラメータを変えて試してみる(minNeighbors編) | Workpiles ...

2016年12月27日 · Toshimitsu Takahashi

TensorFlow が正式に Windows サポートして GPU が使えたので試してみた

Google から正式に Tensorflow が Windows 対応して GPU が使えるとのアナウンスがありました。 https://developers.googleblog.com/2016/11/tensorflow-0-12-adds-support-for-windows.html セットアップ環境 OS) Windows 10 Pro GPU) NVIDIA GeForce GTX 960 この環境で TensorFlow を試しみたいと思います。なるべくDドライブにセットアップするようにしています。 CUDA と cuDNN をインストール CUDA Toolkit 8.0 https://developer.nvidia.com/cuda-downloads Operating System: Windows Architecture: x86_64 Version: 10 Installer Type: exe (network) 普通にインストーラを実行しました。 cuDNN v5.1 開発者アカウントを登録して利用規約に同意してダウンロードしました。 https://developer.nvidia.com/rdp/cudnn-download cuDNN v5.1 Library for Windows 10 を落として zip 内の cuda フォルダ内を C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0 にコピー展開します。 Python 3.5 のセットアップ Windows の場合 Anaconda を使った方が後々楽なのでこちらを奨めます。 (2017/01/08 追記) http://tilfin.hatenablog.com/entry/2017/01/08/220556 内に記載してます。 ...

2016年11月30日 · Toshimitsu Takahashi

AWS IAM Switch Roleのリストを増やすChrome Extensionを作った

AWS Management Console において、AWSアカウントから他のAWSアカウントにスイッチロールできますが、これの最近の履歴が5つまでしか表示されない。あくまでも最近使ったものという位置付けなんだと思います。 某仕事上、1つのアカウントから多数の他のアカウントに Switch Role にせざる得ないので専用の URL をブックマークしていました。しかし、これでも毎回補完された入力フォームが挟まれて怠い(このページ遷移が結構かかります)。 このような理由で、Switch Role の履歴(リスト)を拡張する Chrome Extension を作りました。Chrome Web Store で公開しているので簡単にインストールできます。 chrome.google.com 特徴 Switch Role の履歴に設定に定義されたプロファイル(アカウント)の分が項目増えるリスト拡張される。 リストには <プロファイル名> | <AWSアカウントID> と AWSアカウントID がわかりやすくなる。 色指定ができる(固定色ではない)。 スイッチロール後に黒いヘッダーの下部に指定色のバーが表示されてより現在のプロファイルが識別しやすくなる。 Chrome Sync (端末間) で設定は共有されます。 設定 ブラウザの URL バー右に並ぶ拡張のアイコンをクリックして、ポップアップしたテキストエリアに CLIで同様の設定となる ~/aws/config ファイルと同様のフォーマットで定義して保存するだけです。色指定 (例.color = ffcc99) も CSS ライク(先頭 # なし)で指定できます。GitHubの README を詳しくは参照ください。 GitHub - tilfin/aws-extend-switch-roles: Extend your AWS IAM switching roles by Chrome extension

2016年8月9日 · Toshimitsu Takahashi

GitLab CI から Google Cloud Pub/Sub 経由で自動デプロイ

自分用のちょっとしたWebサービスをさくら VPS環境に構築しています。こんなオレオレサービスでも Git リポジトリの master ブランチに push したら自動的にデプロイできるようにしたい。毎回 SSH して手動で更新はしたくない。 一般的な方法だと、どうしてもデプロイ先のサーバーにリクエストの受け先を用意する必要があります。概ね HTTP 経由で受けないといけないので、そこから派生するアクションの内容から鑑みても、セキュリティ的にグローバル空間に置かれたサーバでやりたくないものです。 そこで自前で Pub/Sub サービスを経由してやってみることにしました。Pub/Sub であれば懸念事項のアクセスを受け入れることなく、Subscriber も Pull 型で設置できます。 Google Cloud Console まず Google Cloud Console - 認証情報 でサービスアカウントを作成しておきます。次に Google Cloud Console - Pub/Sub でトピックを作成します。そしてそのトピック対して用意したサービスアカウントにPub/Sub サブスクライバ―とPub/Sub パブリッシャーの権限を与えます。 自動デプロイの仕組みを作る スクリプトは Node.js を使います。gcloud モジュールをグローバルで使えるようにしておきます。下記の package.json の定義とおり サブスクライバーとなる agent.js と通知をする publish.js をそれぞれ作ります。 package.json 1 2 3 4 5 6 7 8 9 10 11 { "name": "myapp", "scripts": { "agent": "NODE_PATH=`npm root -g` node agent.js", "publish": "NODE_PATH=`npm root -g` node publish.js" }, "dependencies": { "gcloud": "^0.36.0" }, "private": true } 通知 GitLab CI から呼び出して Pub/Sub にデプロイメッセージを発行するスクリプトを作ります。 ...

2016年7月21日 · Toshimitsu Takahashi

TweetDeckライクなGitHubとBacklogのタイムラインビューワーを作ってみた

GitHub のダッシュボードが絶望的に使いづらいので、タイトル通りタイムラインビューワーを作りました。 tilfin.github.io GitHub のイベントダッシュボードは「誰が何をしたのか」はわかるのですが、結局情報が不足してるためにわざわざ詳細ページに飛ぶ必要があります。そこをどうにかしたかったのが開発理由です。また普段 Nulab さんの Backlog を使っているので、こちらのプロジェクト更新履歴も同じく表示できるようにしました。 サイトの [See a Demo] ボタンからデモとして、GitHub の Public イベントといくつかの有名リポジトリと組織のタイムラインの見られます。(これは非認証状態での API 利用なので社内からのアクセスですと IP アドレスのリミットでエラーになるかもしれません。) 両方とも API がクロスオリジンで呼び出せたので単純な静的ページアプリ(SPA)として作って Github Page で公開しました。そのため各 API のアクセストークンやタイムラインの情報は全てブラウザのローカルストレージに保存する仕様です。JSON でインポート・エクスポートできるのとレスポンシブデザインにしているため、PC で設定をエクスポートして、スマホでホーム画面登録してインポートすることも可能です。 以下は設定方法を説明しておきます。 設定方法 [Get Started Now] ボタンからタイムラインの編集画面に飛びます。追加するタイムラインのサービス毎に [GitHub], [Backlog] ボタンがあります。下記スクリーンショットはそれぞれの My Timeline を追加したものです。カラム幅と通知のオンオフが設定できます。 GitHub タイムラインの追加 まずアクセストークンを生成してください。 https://github.com/settings/tokens から [Generate new token] ボタンをクリックして、「Select scopes」から repo, admin:org/read:org, notifications を選択して、トークンを下記の最初の [Access Token] にセットします。 2番目は GitHub Enterprize を使っている場合は、ルート URL を入れます(この機能は自分自身が利用していないので未確認です)。GitHub.com の場合は空のままにして、[Activate] ボタンを押すと認証が通ると下部の組織タイムラインもしくはリポジトリ(例.microsoft, facebook/react)の指定、または GitHub 全体の Public タイムラインを追加できます。 ...

2016年4月4日 · Toshimitsu Takahashi

AWS EC2にインスタンス一覧から選択してSSHできるスクリプトを作った

EC2 に SSH する度に AWS コンソールからインスタンスの詳細選んで Elastic IP をコピペしてキーファイルを指定して実行というのが怠いので便利スクリプトを作ってみた。 単に SSH 第1引数にプロファイル名を指定します。 ~/.aws/credentials に定義されている中から選択します(引数なしで実行するとプロファイルがわかります)。 $ ec2ssh profile1 0) server-dev 60.100.45.XXX i-abcdefgx keypair-dev 1) server-stg 60.100.45.YYY i-abcdefgy keypair-stg 2) server-prod 60.100.45.ZZZ i-abcdefgz keypair-prod Input target EC2 number> 2 Last login: Wed Jan 20 15:19:00 2016 from xxxx.xxxx.xxx.xxx __| __|_ ) _| ( / Amazon Linux AMI ___|\\___|___| https://aws.amazon.com/amazon-linux-ami/2015.09-release-notes/ \[ec2-user@ip-10-0-0-1 ~\]$ ファイルをEC2からローカルにコピー EC2 の /tmp/log.tgz をローカルの ~/work/ にコピーする。 引数に get, EC2内ファイルパス, ローカルコピー先パス の順に指定する。 コピー先が未指定ならカレントディレクトリになる。 ...

2016年2月25日 · Toshimitsu Takahashi

Mongoose で完全なる ES6 Promise を使う

現行の Mongoose 4.2.4 ではネイティブ Promise がサポートされていない。Query オブジェクトの exec メソッドを引数無しで呼び出すと、Mongoose 独自の Promise が返るがチェインの中で利用すると上手く動かなかった。 [http://mongoosejs.com/docs/api.html#promise_Promise] リンク先に Mongoose 5.0 でネイティブ Promise がサポートされるとあるが今使いたい。そこで Query オブジェクトに execPromise メソッドを拡張実装してみる。 1 2 3 4 5 6 7 8 9 10 11 var mongoose = require('mongoose'); mongoose.Query.prototype.execPromise = function() { var self = this; return new Promise(function(resolve, reject){ self.exec(function(err, data){ if (err) reject(err); else resolve(data); }); }); } 以下のようにチェインで使うことができる。 ...

2015年11月9日 · Toshimitsu Takahashi