このエントリーをはてなブックマークに追加

ご挨拶

はじめまして!
4月半ばにカルテット開発部にジョインしました下田と申します。
今後ともどうぞ宜しくお願いいたします!m(_ _)m

来歴

社会人になってすぐはPHPとかJavaとかインフラとかいろいろ経験した後、途中でRailsエンジニアに鞍替え。
1年ほどでまたPHPに戻り、SymfonyをメインにWebアプリケーションエンジニアとして働いています。

入社のきっかけ

転職を考え始めたときに、諸々の事情で地方への移住が決定しており、なかなか具体的な転職活動に踏み切れていなかったところ、
Symfony Meetupで弊社CTOのたつきちさんより

「そういう事情なら、完全リモートワークで弊社はいかがですか?」

とお声がけ頂いたのがきっかけでした。

もともとこの技術ブログの読者で会社については知っていたこと、
個人的にSymfony力や設計力を伸ばしていきたいと考えていたこともあり、
二つ返事で入社させていただく運びとなりました。笑

働き方について

そんな訳で、本社は名古屋ですが、現在は神奈川の自宅から毎日仕事をしています。
(この記事も現在進行系で自宅のリビングで書いています)

カルテット開発部では、遠方からの完全リモート形態での入社は僕が初でしたが、
もともと僕の入社前から半リモート形態(週に数回出社や時短出社+自宅作業など)の働き方をされているエンジニアさん達が既に数名いらっしゃったので、
場所に縛られずに開発を進めるための仕組みはほぼ揃っていました。

おかげさまで、入社から2ヶ月が経とうとしておりますが、何の問題もなく仕事を進めることができています :smiley:

以下、リモートで働く上での仕組みや工夫、実践していることをを幾つか紹介します。

定期MTGはSlackビデオで

毎朝9時に開発部の朝礼があるのと、週次で担当領域ごとのチームMTGが行われるのですが、
どちらもビデオ通話を駆使して実施されています。

互いに顔が見えた状態の通話なので、例えば同じ空間にいないと伝わらないような「温度感」とか「表情」などの非言語的なコミュニケーションについても共有できています。

画面越しでうんうん、とうなずくだけでも、話し手としては安心しますよね。

ビデオ会議の様子
ビデオ会議の様子

情報共有はドキュメントベースで柔軟に

開発上必要になってくる情報についてはesa上でドキュメント化が徹底されていたり、
また仕様の相談などもGitHubのプルリクエスト上で気軽に聞けたりなど、
「情報をできる限りオンライン上で残す」という文化が根付いています。

おかげで欲しい情報にすぐアクセスできるため、1人離れたところにいても情報難民になることがないような環境が整っています。

フォローアップMTG

フォローアップMTGという立て付けで、メンターと1on1で何を話しても良い時間を毎日5分程度設けてもらっています。(こちらもビデオ通話で行っています)
当初は入社直後の「困ってることないですか?」の声掛けタイムでいろいろ相談していたのですが、入社2ヶ月経った今も継続してもらっています。

最近だと思いっきり雑談で終わってしまったりもするのですがw
「気になることがあるが、わざわざ誰かに声をかけて時間を割いて聞くほどのことでもないし、どうしよう…」的なモヤモヤが発生しても、
毎日のフォローアップMTGで解消できるため頭の中で引きずることがなく、ストレスになりにくいです。

他愛のない話でも共有できていると、他の事に関してもまず相談することに対してのハードルが下がりますよね。
とかくリモートワーカーは疎外感を感じがちですが、カルテットでは全く陥ることなく毎日過ごすことが出来ています。

分報チャンネル

※分報とは?という方はこちらを参考に

開発メンバーはそれぞれ、自分専用のどのように利用しても良いチャンネルを1人1つずつ割り当てられています。
使い方は各々、作業進捗共有だったりbotの住処にしたり様々なのですが、
さながら社内用twitterのように、個人的なつぶやきを投稿する場のようにも働いています。

カレー記念日
ある日の分報チャンネルのひとコマ

なんでもないつぶやきでも、反応があるとつながりを感じますよねw
このあたりの仕掛けも、疎外感の解消に役立っている気がします。

最後に

以上ここまで、カルテットでのリモートワークの様子を一部ですが紹介させていただきました。
これ以外にも、エンジニアにとって働きやすい環境である理由はたくさんあるのですが、
またの機会に紹介させていただければ、と思います :smiley:

カルテットでは場所に縛られない働き方を求めるエンジニアを募集しています!


このエントリーをはてなブックマークに追加

カルテット開発部の後藤です。
弊社ではアプリケーションの開発にSymfonyを使っており、ロギングはSymfonyに標準でバンドルされているMonologを使っています。 使っている期間はそこそこ長いのですが、ログのための設定を記述する頻度が少ないことや、どうもMonologの設定の概念がつかめていないためか、いつもいろいろ迷いながら設定しています。 いつも迷って困るので、今後のために概念などを整理しました。ついでに記事にして公開します。

Monologの基礎概念

Monologはロギングのためのライブラリですが、物理的なログ出力機構に関する詳細をアプリケーションから意識することなく透過的にロギングできるように、多少抽象化されています。 Monolog内に登場する概念と役割は、次のようになっています。

ロガー
アプリケーションから利用するエントリポイントオブジェクトです。PSR-3のLoggerInterfaceを実装しています。

チャンネル
Monolog内では、ロガーをインスタンス化する際に付与する識別名がチャンネルと呼ばれます。つまり、ロガーインスタンスの1つ1つがチャンネルに相当します。アプリケーションからロガーに対して直接ロギングするのではなく、チャンネルに対してロギングするというニュアンスからこうなっているのだと思われます。

ハンドラー
物理的なログ出力処理を担当します。様々なハンドラーが用意されています。1つのロガーに、複数のハンドラーを登録できます。また、複数のハンドラーを束ねて特殊な機能を付加する抽象ハンドラーもあります。

プロセッサー
ロガーやハンドラーに対して、ログ行ごとの付加情報の処理を担当します。ロガーおよびハンドラー1つに対して、複数のプロセッサーを登録できます。

フォーマッター
ログ行を出力先へ書き出す際のフォーマット処理を担当します。フォーマッターは、1つのハンドラーにつき1つだけ設定できます。

Monologの構成要素

複数のチャンネル(ロガー)を構成する場合

アプリケーションの標準のログ、特定のバッチ処理専用のログというように、複数のチャンネルを構成する場合は、Monolog的には異なるロガーインスタンスを作るということになります。 それぞれのチャンネル(=ロガー)に対して、出力先の設定をハンドラーで構成し、ロガーに渡します。 以下はMonologのドキュメントに記載されている、複数のチャンネルを構成するコード例です。my_loggersecurity という2つのチャンネルを構成していますが、どちらもまったく同じハンドラーを登録しています。

<?php

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\FirePHPHandler;

// Create some handlers
$stream = new StreamHandler(__DIR__.'/my_app.log', Logger::DEBUG);
$firephp = new FirePHPHandler();

// Create the main logger of the app
$logger = new Logger('my_logger');
$logger->pushHandler($stream);
$logger->pushHandler($firephp);

// Create a logger for the security-related stuff with a different channel
$securityLogger = new Logger('security');
$securityLogger->pushHandler($stream);
$securityLogger->pushHandler($firephp);

// Or clone the first one to only change the channel
$securityLogger = $logger->withName('security');

ハンドラー個別の内容

よく使われるハンドラーや、特殊な動きをするハンドラーもあるので、いくつかを紹介します。

StreamHandler

シンプルなファイル出力用のハンドラーです。

RotatingFileHandler

StreamHandlerを継承したハンドラーです。ログを日付別のファイルに分割し、一定数より古いものは自動的に削除します。

FingersCrossedHandler

任意の1つのハンドラーをラップして使う抽象ハンドラーで、特定の条件(指定したレベル以上のログが記録された等)を満たさない限り何もせず、条件を満たした際に、それまで発生したログを含めて内部のハンドラーを実行します。FingersCrossedハンドラーの handle() メソッドのコードは次のようになっています。ログをバッファリングする機構が入っていることが分かりますね。 $this->activate() の箇所で、バッファリングしたログレコードすべてに対して handle() が呼び出されます。

ソース

// Monolog/Handler/FingersCrossedHandler.php
public function handle(array $record)
{
    if ($this->processors) {
        foreach ($this->processors as $processor) {
            $record = call_user_func($processor, $record);
        }
    }

    if ($this->buffering) {
        $this->buffer[] = $record;
        if ($this->bufferSize > 0 && count($this->buffer) > $this->bufferSize) {
            array_shift($this->buffer);
        }
        if ($this->activationStrategy->isHandlerActivated($record)) {
            $this->activate();
        }
    } else {
        $this->handler->handle($record);
    }

    return false === $this->bubble;
}

WhatFailureGroupHandler

任意の1つ以上のハンドラーをラップして使う抽象ハンドラーで、配下の複数のハンドラーを、ハンドラー内のエラー発生を無視してすべて実行します。WahtFailureGroupハンドラーの handle() メソッドのコードは次のようになっています。What failure? がどのようなことを指しているのか、コードを見ればすぐに分かりますね。

ソース

// Monolog/Handler/WhatFailureGroupHandler.php
public function handle(array $record)
{
    if ($this->processors) {
        foreach ($this->processors as $processor) {
            $record = call_user_func($processor, $record);
        }
    }

    foreach ($this->handlers as $handler) {
        try {
            $handler->handle($record);
        } catch (\Exception $e) {
            // What failure?
        } catch (\Throwable $e) {
            // What failure?
        }
    }

    return false === $this->bubble;
}

Symfonyでの設定

SymfonyでロギングにMonologを利用する場合、ほとんどMonologに用意されている概念をそのままSymfonyの設定ファイルで記述することになります。

Symfonyでのログ

設定ファイルに記述した内容は、MonologBundleのエクステンションでMonologのLoggerオブジェクトを起点とするオブジェクトグラフに変換され、DIコンテナに登録されます。 アプリケーションコードからロギングするには、@logger サービスをクラスにインジェクトするなり、サービスコンテナから取り出すなりして使います。

Symfonyでの複数チャンネルの設定

デフォルトでは @logger だけが用意されており、つまり、チャンネルも1つのみです。 複数のロガーを構成する場合は、コンフィギュレーションで明示的にチャンネルを用意します(monolog.channels)。さらにハンドラー側の channels にて、「どのチャンネルに結びつけるのか」を指定します。ハンドラー側に channels を何も指定しない場合は、すべてのチャンネルに対するロギングで使われるハンドラーになります。

// app/config/config_prod.yml または config/packages/prod/monolog.yaml
monolog:
    channels: ['foo', 'bar']
    handlers:
        special_log:
            type: rotating_file
            path: '%kernel.logs_dir%/special.log'
            level: info
            max_files: 10
            channels: [foo] # このハンドラーは、fooチャンネル用ロガーで使う

このように設定すると、SymfonyのDIコンテナ内に @monolog.logger.foo というサービスが登録され、foo チャンネル専用のロガーとして使えるようになります。foo チャンネルへのロギングを行いたいクラスには、このように特定チャンネル専用のサービスを注入して利用します。

SymfonyでのFingersCrossedHandlerの設定

Monologに用意されているFingersCrossedHandlerをSymfonyでそのまま利用できます。fingers_crossed を利用する場合、まず記録したいログのハンドラー(下の例では nested)を普通に記述しておき、その上にかぶせる形で fingers_crossed を設定します(下の例では main)。fingers_crossed ハンドラーの action_level には、ログを実際に出力するトリガーとなるエラーレベルを指定します。

// app/config/config_prod.yml または config/packages/prod/monolog.yaml
monolog:
    handlers:
        main:
            type: fingers_crossed
            action_level: error
            handler: nested
        nested:
            type: stream
            path: '%kernel.logs_dir%/app.log'
            level: info

SymfonyでのWhatFailureGroupHandlerの設定

ログのハンドラーによっては、ログ記録処理でエラーや例外が発生してしまうケースがあります(例えば外部へ通信を行うSlackHandlerなどの場合)。ログ記録処理で発生する例外を単に無視する場合に、whatfailuregroup ハンドラーを利用します。members に、このハンドラーに紐付ける子ハンドラーを指定します(配列形式)。

// app/config/config_prod.yml または config/packages/prod/monolog.yaml
monolog:
    handlers:
        main:
            type: whatfailuregroup
            members:
                - nested
        nested:
            type: stream
            path: '%kernel.logs_dir%/app.log'
            level: info

参考資料

Laravelでの設定

参考のために、Symfony以外のフレームワークでのMonologの使われ方として、Laravelを調べてみました。
Laravelの場合は、Monologの構造をそのまま使うのではなく、フレームワークが用意したログ用のクラスとコンフィギュレーションで設定を行えるようになっています。Monologは、フレームワークのロガーが利用するドライバーの内部実装として使われているという位置づけです。

Laravelでのログ

参考:Laravelのログ

おわりに

Monologの基礎概念と、Symfonyでの設定方法の概要、およびLaravelでの使われ方を簡単に紹介しました。 一度概念的に整理しておけば、設定する際もスッキリと記述していけますよね。 Monologは非常に小さなライブラリなので、コードを読んで構造や概念を掴むのにさほど時間を必要としません。 積極的にライブラリのコードを読んでいきたいですね。


カルテット開発部では、ライブラリのコードを読んで仕組みから理解したい仲間を募集しています!


このエントリーをはてなブックマークに追加

はじめに

タイトルのとおりですが、意外と苦労したのでやり方をまとめておきます。

使う道具

  • CirleCI 2.0
  • markdown-pdf
    • MarkdownをPDFに変換するためのCLIツール
    • npmでインストール
  • gdrive
    • Googleドライブを操作するためのCLIクライアント
    • PDFのアップロードに使う
    • バイナリをダウンロードしてインストール
    • ソース(golang)をコンパイルしてインストール(後述)

具体的な方法

事前準備

CircleCIからGoogleドライブにファイルをアップロードするためには、サービスアカウント での認証が必要なので、以下のとおり準備しておきましょう。

  1. Google API Console でサービスアカウントを作る
  2. デプロイ先にしたいGoogleドライブ上のフォルダを、サービスアカウントに共有する image
  3. CircleCIのプロジェクト設定で、サービスアカウントの認証情報のJSON文字列を一行化して環境変数として登録する image

markdown-pdfでのPDF生成方法

例えば、src/ 配下にMarkdownファイル群が階層構造で格納されているとして、それらを同じ階層構造で build/ 配下に拡張子だけを .pdf に変えて出力したい場合、以下のようなシェルスクリプトで実現できます。

for file in `find src -type f` ; do
    name=`echo $file | sed -r 's/src\\/(.*)\\.md/\\1/'` # src/<ファイル名>.md から <ファイル名> を取り出す
    npx markdown-pdf -s css/style.css -o build/$name.pdf $file
done

-s css/style.css でレンダリングに使用するCSSファイルを指定しています。これは本来は必須ではありませんが、こちらで指摘されているとおり、デフォルトのCSSだと [text](url) というMarkdownが text (url) という表記に変換されてしまうため、これを回避するために最低限以下のCSSを当てる必要があります。

abbr[title]:after,
a[href]:after {
    content: "";
}

GitHub風のCSSを適用

markdown-pdfはMarkdownパーサーとして remarkable を使っており、remarkableはデフォルトで GFM(GitHub Flavored Markdown)をコンパイルできるようです

せっかくなのでGitHub風のCSSを適用して、それらしい見た目でPDFが生成されるようにしておきましょう。

とりあえず今回は泥臭く、github-markdown-cssのcss をコピペして .markdown-bodybody に置換しました。フォントサイズも必要に応じて変更するとよいでしょう。

body に置換すればいいと判断したのは、markdown-pdfのデフォルトのcss がそうなっていたからです。

ワンライナー化してpackage.jsonにscriptsとして追加

以下のようにワンライナー化して、npm scriptsから実行できるようにしておくと便利です。(ついでに、ビルド時に build/ 配下を一旦全削除するようにしてあります)

{
  "dependencies": {
    "markdown-pdf": "^8.1.1"
  },
  "scripts": {
    "build": "rm -rf build/* && for file in `find src -type f` ; do npx markdown-pdf -s css/style.css -o build/`echo $file | sed -r 's/src\\/(.*)\\.md/\\1/'`.pdf $file ; done"
  }
}

gdriveでのアップロード方法

gdriveコマンドで build/ ディレクトリを丸ごとGoogleドライブの特定のフォルダ配下にアップロードする方法は、以下のとおりです。

gdrive --config $(pwd) --service-account credential.json upload -p <ここにデプロイ先フォルダのID> -r build

credential.json が置かれている場所を --config で指定する必要があります。--config の値はデフォルトでは $HOME/.gdrive になっています。ここでは、credential.json がカレントディレクトリに置いてある想定で、 $(pwd) としています。

後述しますが、--service-account オプションは2018/05/14現在の最新版v2.1.0にはまだ入っていません。最新のソースコードをコンパイルしてインストールしないと使えませんので、ご注意ください。

CircleCIの設定ファイル

上記を踏まえて、CircleCIの設定ファイル .circleci/config.yml を書いてみます。

キャッシングなどを省いて処理の要点だけを書くと、以下のような内容になります。(書式の詳細については 公式リファレンス をご参照ください)

version: 2
jobs:

  # markdown-pdfを使ってPDFを生成するジョブ
  build:
    docker:
      - image: circleci/node
    working_directory: ~/wd
    steps:
      # 日本語のフォントがないと、PDF生成時に日本語部分がレンダリングされない
      - run: sudo apt-get update && sudo apt-get install fonts-ipaexfont -y
      - checkout
      - run: npm i
      - run: npm run build
      # ビルドの成果物(PDFが入っているディレクトリ)をキャッシュしてデプロイジョブに渡す
      - save_cache:
          key: build-{{ .Revision }}
          paths:
            - ~/wd/build

  # gdriveを使ってPDF(が入っているディレクトリ)をデプロイするジョブ
  deploy:
    docker:
      # gdriveをソースからコンパイルするのでgolang環境が必要
      - image: circleci/golang
    working_directory: ~/wd
    steps:
      # gdriveをインストール
      - run: go get github.com/prasmussen/gdrive
      # 成果物をリストア
      - restore_cache:
          keys:
            - build-{{ .Revision }}
      - deploy:
          command: |
            # 環境変数から認証情報を取得してJSONファイルに出力
            echo $GOOGLE_SERVICE_ACCOUNT_CREDENTIAL > credential.json
            # 成果物のディレクトリ名を日時でリネーム
            dirname=`date +%Y%m%d_%H%M%S` && mv build $dirname
            # 成果物のディレクトリを丸ごとデプロイ
            gdrive --config $(pwd) --service-account credential.json upload -p <ここにデプロイ先フォルダのID> -r $dirname

workflows:
  version: 2
  build_and_deploy:
    jobs:
      - build
      - deploy:
          requires:
            - build

build ジョブでPDFを生成して、deploy ジョブでGoogleドライブにデプロイしています。

日本語フォントが何も入っていない環境だとPDFを生成するときに日本語部分がレンダリングされないので、build ジョブのはじめで fonts-ipaexfont をインストールしています。インストールするフォントは何でもOKです。

deploy ジョブのほうでは、CircleCIのコンテナにサービスアカウントの認証ファイルを設置するために、事前準備で登録しておいた環境変数の中身をJSONファイルに書き出しています。また、build というディレクトリ名のままだと分かりにくいので、日時の名前にリネームしてからデプロイするようにしています。

gdriveの最新バージョンがリリースされた暁には

なお、gdriveをソースからコンパイルしてインストールしていますが、これはgdriveの --service-account 機能が まだバイナリとしてリリースされておらず、最新のソースを自分でコンパイルしないと使えない が故の措置です。(README からダウンロードできるv2.1.0には、この機能は入っていません)

v2.2.0(多分)がリリースされた暁には、インストールはバイナリをダウンロードしてくるだけでよくなるため、golang環境も不要になり、以下のようにジョブを1つにまとめることができるようになります。

version: 2
jobs:
  build:
    docker:
      - image: circleci/node
    steps:
      - run: sudo apt-get update && sudo apt-get install fonts-ipaexfont -y
      - checkout
      # gdriveのバイナリをダウンロードして、チェックサムを確認して、実行パーミッションをつける
      - run: |
          wget "<ここにgdrive-linux-x64のダウンロードURL>" -O gdrive
          [ `sha1sum gdrive | awk '{print $1}'` = '<ここにgdrive-linux-x64のshasum>' ]
          chmod +x gdrive
      - run: npm i
      - run: npm run build
      - deploy:
          command: |
            echo $GOOGLE_SERVICE_ACCOUNT_CREDENTIAL > credential.json
            dirname=`date +%Y%m%d_%H%M%S` && mv build $dirname
            ./gdrive --config `pwd` --service-account credential.json upload -p <ここにデプロイ先フォルダのID> -r $dirname

実装例

以下のGitHubリポジトリに実装例を上げてみたので、参考にしてみてください。

https://github.com/ttskch/markdown-pdf-googledrive-ci-sample

実際に動かすと、以下のように日時のフォルダ配下に成果物がデプロイされます。

image

おわりに

簡単なことのようで意外とつまずきポイントが多かったので自分のためのメモも兼ねてまとめてみました。
どこかの誰かのお役に立てば幸いです。