オルトプラスエンジニアの日常をお伝えします!

webサービスのログ運用について

この記事はAltplus Advent Calendar 2017の9日目のエントリです。

こんにちは、サービスのインフラ周りの対応をしている橋本です。 ログの可視化や集計などで、複数サービスに適用を始めたので、 現在のログ運用の構成を書きたいと思います。

ログ運用について

構成

  • fluend
  • KinesisFirehose
  • S3
  • Athena + redash

ログ流れ

fluentd -> KinesisFirehose -> S3

ログ参照

redash -> athena -> s3
aws-cli -> athena -> s3
web -> athena 

監視について

アラート用でボトルネックになる部分とAWSサービス側障害用で下記を監視しております。

  • kinesisFirehose
    • レコード取り込み数
  • fluentd
    • リトライ数

ログフォーマットについて

他のところにも使うかもしれなかったので加工複数の環境に対応できるよう、JSONフォーマットでの書き出しを行っています。

集計時

  • DBのデータとのJOINしたいとき
    • DBデータをTSV出力し、S3アップ後、Athenaのテーブル作成

テーブル

  • kinesisFirehoseでS3保存時に作成されるディレクトリの日付毎でパーティションを何年分かを設定しています。

困ったところ

Athenaのテーブル設定でパーティションを始め、年、月、日で分けていたのですが、クエリを書きずらかったため、 20171212 などひとつのカラムに年月日の形で入れたほうがクエリが簡単になり便利でした

懸念

今後、Athenaで参照するデータがだいぶ増えた場合にクエリにだいぶ時間がかかるようにならないか心配で、 なったときは、bigqueryのほうに引っ越しを考えています。

クエリ

はじめ bigquery で運用していて、S3のログをGCS転送後そちらを bigqueryにロードするよう設定していました。 Athena側に移行する際に、日付パーティションの指定部分の違いとタイムゾーンの切り替えのところで少し悩んでいたので where句のところをメモで残せればと思います。

  • athena
# タイムゾーンのほう
SELECT date_format(time AT TIME ZONE 'Asia/Tokyo' , '%Y%m%d') as event_at,

# パーティションのほう
dt(パーティションのカラム) >= date_format((now() + interval '-30' DAY) AT TIME ZONE 'Asia/Tokyo' , '%Y-%m-%d') )
  • bigquery
# タイムゾーンのほう
SELECT FORMAT_TIMESTAMP('%Y%m', time, 'Asia/Tokyo') AS date_time

# パーティションのほう
_TABLE_SUFFIX >= FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 365 DAY))

所感

  • ログを集約して転送する部分のサーバがなく少し運用が楽になりました
  • まだそこまでのログ量はないのですが、S3にデータを保存するのだと、 圧縮した状態で配置できるので、bigqueryに保存するよりもログ量が増えた際にお得になっていきそうに思います。

Laravelの機能を使って綺麗なコードを書く

この記事はAltplus Advent Calendar 2017の8日目のエントリです。

概要

  • 主にコード量を減らすことが出来る、読みやすくなるようなLaravelの機能を紹介してく
  • 備忘録的な感じ

ルートプレフィックス

どんなもの?

使わないで書くと?

Route::get('/admin',           'Admin\AdminController@top');
Route::get('/admin/hoge',      'Admin\AdminController@hoge');
Route::get('/admin/huga/piyo', 'Admin\AdminController@hugaPiyo');

使って書くと!

Route::prefix('admin')->group(function () {
    Route::get('/',          'Admin\AdminController@top');
    Route::get('/hoge',      'Admin\AdminController@hoge');
    Route::get('/huga/piyo', 'Admin\AdminController@hugaPiyo');
});

メリット

  • ルーティングを見た時に関連のあるルーティングがぱっと見で分かりやすくなる
  • 上記の例の場合だと、「管理者画面関連のルーティングなんだろうなぁ」と予想が付く

名前空間ルーティング

どんなもの?

  • 同じ名前空間を持つコントローラーを一つにまとめることが出来る
  • 例えば、app/Http/Controllers/Admin/配下にコントローラーファイルを複数作成していた場合にまとめて書ける

使わないで書くと?

Route::prefix('admin')->group(function () {
    Route::get('/',          'Admin\AdminController@top');
    Route::get('/hoge',      'Admin\AdminController@hoge');
    Route::get('/huga/piyo', 'Admin\AdminController@hugaPiyo');
});

使って書くと!

Route::prefix('admin')->namespace('Admin')->group(function () {
    Route::get('/',          'AdminController@top');
    Route::get('/hoge',      'AdminController@hoge');
    Route::get('/huga/piyo', 'AdminController@hugaPiyo');
});

メリット

  • ルートプレフィックスと同じ

リソースフルルーティング

どんなもの?

  • 基本的なCRUD関連のルーティングをまとめて書ける

使わないで書くと?

Route::prefix('users')->group(function () {
    Route::get   ('/',            'UserController@index');
    Route::get   ('/create',      'UserController@create');
    Route::post  ('/',            'UserController@store');
    Route::get   ('/{user}',      'UserController@show');
    Route::get   ('/{user}/edit', 'UserController@edit');
    Route::patch ('/{user}',      'UserController@update');
    Route::delete('/{user}',      'UserController@destroy');
});

使って書くと!

Route::resource('users', 'UserController');

メリット

  • コード量かなり減らせる
  • 一目でそのコントローラーがCRUDの要素を持っていることが分かる
  • コントローラー内のアクションメソッドも同じになるので、共通認識での開発ができる

備考

  • リソースフルルーティングに対応するコントローラーは、下記のようなコマンドで一発で作成できる
    php artisan make:controller UserController --resource

フォームリクエストバリデーション

どんなもの?

  • バリデーションをapp/Http/Requests/配下に作成されるフォームリクエストクラスに書くことが出来る

使わないで書くと?

  • 下記のコードをモデルやサービスクラスでメソッド化して、コントローラで呼び出さなければならない
$request->validate([
    'name'  => 'required',
    'email' => 'required|email',
]);

使って書くと!

  • 下記のようなコマンドでフォームリクエストクラスを作成する
    php artisan make:request StoreBlogPost
  • app/Http/Requests/StoreUser.phpが作成されるので、下記のように修正する
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreUser extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name'  => 'required',
            'email' => 'required|email',
        ];
    }
}
  • コントローラーのアクションメソッドで、上記のクラスを引数に指定して呼び出すだけで、バリデーションを通ったもののみがアクションメソッドの処理を実行する
public function store(StoreUser $request)
{
}

メリット

  • バリデーションについてのコードをフォームリクエストクラスに任せることができる
  • (今回は解説しないが)認証機能、エラーメッセージの設定も出来る

最後に

「Laravelにはこんな機能もあるよ!」等のコメントいただけると嬉しいです!

Wantedlyとぼくと自動化

この記事は Altplus Advent Calendar 2017 の7日目のエントリです。

※Qiitaに書いた記事を転載しています。

こんにちは、自動化大好き オルトプラスラボの嶋田大輔(id:cimadai) です。またお会いしましたね。

概要

今日は私の会社で(もしかしたら皆さんの会社でも)よくある、ちょっとした手間を一つ減らして効率化していこう、という内容を書きます。

どのような問題なのか

私の会社では、Wantedlyを活用していて、特にブログに注力しています。 Wantedlyのブログは会社の採用に繋がるということで有用なツールの一つとして多くの企業に使われていると思います。

多くの企業に使われているということはそれだけ多数の記事があるため、効果的に認知を高めるにはその中でどうにか目立たせないといけません。

幸いWantedlyにははてなのホッテントリのような、短期間に「いいね」が多数集まった記事を「急上昇」として取り上げる機能があります。

そこで、記事が書かれた後は社員に対して「いいね」をするよう「お願い」があります。1

もちろん、いい方に来て頂きたいので当然「いいね」するわけなのですが、記事を開き下までスクロールし、その間に目にとまる文章があれば読んでしまったりして割と時間泥棒だったりするのです。

ということで、どうにか記事を読むことなくできるだけ手間なく「いいね」できる方法はないかと思い、この記事を書くに至りました。

なにをしたのか

AutedlyというChrome Extensionを作り、記事を開いたら自動的に「いいね」するようにしました。

github.com

解説

Chrome Extensionを作る方法はいろいろあるので別記事に譲りますが、ここではどんなことをしたのかを解説します。

manifest.json

{
    "manifest_version": 2,
    "name": "Autedly",
    "version": "0.2.0.0",
    "description": "Auto like when you open wantedly page.",
    "background": {
        "scripts": ["background.js"],
        "persistent": true
    },
    "content_scripts": [
        {
            "matches": [
                "*://www.wantedly.com/companies/altplus/*"
            ],
            "css": ["jquery.toast.min.css"],
            "js": ["jquery-2.1.4.min.js", "jquery.toast.min.js", "content.js" ],
            "run_at": "document_start"
        }
    ],
    "web_accessible_resources": [
        "background.js"
    ],
    "permissions": ["*://*/*"]
}

↑はExtensionの定義をしています。 簡単なDOM操作のためにjQueryを使い、ユーザーに対するフィードバックにjQuery Toastというライブラリを使いました。

実際の処理は以下になります。 Wantedlyではブログがたくさん並んでいる企業ページと、各記事個別のページがあるので、それぞれで処理を分けています。

コード中のコメントにも書いてあるのですが、Wantedlyの記事読み込みタイミングや「いいね」状態の反映のタイミングが非同期でなかなか掴めなかったので、setTimeoutを利用しています。 ここはかなりイケてない部分なので気が向いたらどうにか直したいところです。

その後chrome.runtime.sendMessageでbackground taskに対して処理を依頼しています。

content.js

function likeIfNeeded($target) {
    var url = location.href;
    var re = /.*\/companies\/altplus\/post_articles\/(\d+)/i;
    var found = url.match(re);
    if (found) {
        var articleId = found[1];
        var likeUrl = 'https://www.wantedly.com/api/internal/posts/' + articleId + '/like';

        var $likeButton = $target.find(".post-like-button");
        if (!$likeButton.is(".liked")) {
            var csrfToken = $("meta[name=csrf-token]").attr("content");
            chrome.runtime.sendMessage({command: "like", csrfToken: csrfToken, likeUrl: likeUrl});
            // TODO: 単純にlikedを付けるだけだとその後もう一度ボタンを押した時に動作がおかしい。
            $likeButton.addClass("liked");
            $.toast({
                text: "「いいね!」しました。",
                position: "top-right"
            });
        } else {
            console.log("Already liked.");
        }
    }
}

$(function () {
    // 企業ページから各記事を開いた時
    $(document).on("click", ".stream-post", function() {
        var $self = $(this);
        // setTimeoutはイケてない
        setTimeout(function () { likeIfNeeded($self); }, 1000);
    });
    // 記事を直接開いた時
    // setTimeoutはイケてない
    setTimeout(function () { likeIfNeeded($("body")); }, 1000);
});

background taskでは、content.jsから受け取った「いいね」をつけるURLに対してXHRでPOSTを行い、「いいね」しています。 すでに「いいね」済みの記事に対して実行すると442が返ってくるのですが、基本的にcontent.js側で判定しておりめったにこないはずですので、Wantedlyサーバーに負荷がかかりすぎるということはないでしょう。 2

こんなことをしなくともcontent.js側でjQueryを使っているんだから$.ajaxでPOSTすればいいじゃないかとも思ったのですがHTTP OnlyのCookieが必要なのでこうなりました。

background.js

chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
    if (request.command == "like") {
        xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            switch ( xhr.readyState ) {
                case 4: // データ受信完了.
                    if( xhr.status == 200 || xhr.status == 422 ) {
                        console.log('LIKED!');
                    }
            }
        };

        xhr.open("POST", request.likeUrl, true);
        xhr.setRequestHeader("x-csrf-token" , request.csrfToken);
        xhr.setRequestHeader("Content-Type" , "application/x-www-form-urlencoded; charset=UTF-8");
        xhr.setRequestHeader("x-requested-with" , "XMLHttpRequest");
        xhr.send("location=post_action_bar");
    }
    return true;
});

動作

f:id:d-shimada:20171206013048g:plain

「いいね」がない状態でページを読み込むと自動で「いいね」が付けられる、という動作風景です。 右上にヒョコっとでるのがjQuery Toastです。 4日目の記事にも書いたのですが、最近はあまりjQueryを多用しなくなったので久しぶりのjQueryという感じでしたがこういうちょっと書捨て、みたいな感じだととても楽ですね!

社内限定公開

さて、このような感じで作ったExtensionですが、気軽に使えないと困ってしまうので社内限定で公開してみました。

オルトプラスの方は「こちら」からインストールできます。

f:id:d-shimada:20171206013201p:plain ※社内限定公開なので検索しても出てこないです。

社内限定であれば無料ででき、かなり気軽に配付できることができたので楽ちんでした。

公開方法は以下の記事がとても参考になりました。

blog.serverworks.co.jp

まとめ

ちょっとの時間のロスが通年でみれば大きなロスになることってありますよね。 こうしたExtensionを駆使することでWantedlyに負荷をかけることもなく、会社は「いいね」が集まって嬉しい、自分は時間が節約できて嬉しいといういいこと尽くめなプチハックでした。

それではまた別の日に!


  1. もちろん重要性はわかってますよ!ぼくは真面目な社員ですからね!

  2. なので大目に見て頂けないですかね。。