‘PHP’ カテゴリーのアーカイブ

user icon

RoundcubeをPeachPieで.NETの上に載せてみた話

TL;DR

  • Roundcube 1.7.5 を PeachPie で .NET にコンパイルして、Kestrel 上で動かしました。
  • 普通に使える状態です。ログイン、閲覧、送受信、添付、スペルチェック、LDAPアドレス帳まで確認しました。
  • Roundcube 本家のユニットテストは、PHP 8.4 で通る 1,262 件が全部通るところまで追い込みました。そのために PeachPie 側に30本ちょっとのパッチを当てています。

きっかけ

弊社では Roundcube を長いこと使っていて、カスタマイズもいろいろやってきました。
ある日ふと「PHP を .NET で動かす PeachPie ってありましたよね。あれで Roundcube ってそのまま動くんでしょうか?」と思いまして。
動けば PHP-FPM なしで dotnet 一発で起動できます。.NET のライブラリと混ぜられる可能性もありますし、とりあえずやってみることにしました。

PeachPie って何

PHP のソースを .NET のアセンブリ(DLL)にコンパイルしてくれるコンパイラです。
ASP.NET Core のミドルウェアとして PHP のサイトを載せられるので、WordPress を動かすデモなどが公開されています。

ただ、NuGet で配布されている版は少し前のもので、PHP 8.1 までの構文で止まっています。
Roundcube 1.7 は新しい構文を使っているので、今回は GitHub の master をソースからビルドして使いました。

とりあえず動かす

構成はシンプルです。

  • アプリ: Roundcube を PeachPie でコンパイルした DLL + Kestrel(コンテナ1個)
  • DB: MySQL(コンテナ1個)
  • 前段: 既存のリバースプロキシから振り分け

コンテナは rootless の Podman で、systemd のユーザーユニットで起動しています。
設定は、既存の Roundcube 環境の config.inc.php をほぼそのまま持ってきました。
PeachPie では設定ファイルもコンパイル対象なので、設定を変えるたびにビルドし直しになります。

<!-- Website.msbuildproj(抜粋イメージ) -->
<Project Sdk="Peachpie.NET.Sdk/1.2.0-xxx">
  <ItemGroup>
    <Compile Include="**/*.php" Exclude="tests/**;vendor/**/tests/**" />
  </ItemGroup>
</Project>

ビルドが通ってログイン画面が出たときは、ちょっと感動しました。
……が、ログインしたら日本語のメールが化けました。ここからが本番です。

ハマりどころ: 文字列は「文字」か「バイト」か

PHP の文字列は、ただのバイト列です。strlen() はバイト数を返しますし、$s[0] は1バイトです。
一方の PeachPie は、なるべく .NET の文字列(UTF-16)のまま扱おうとします。その結果、こうなります。

  • strlen('あ') が 3 ではなく 1 になる
  • ord() がバイト値ではなく文字コードを返す
  • 正規表現の [\x80-\xFF] がマッチしない

メールまわりはまさにバイト列の世界なので、これがあちこちに効いてきます。
例えば IMAP の「リテラル」は {123} のようにバイト数を前置きしてデータを送ります。ここが文字数で数えられていると、日本語の件名を含むメールでサーバーとの会話がずれてしまいます。

対策としては、PeachPie 側で「このデータはバイト列として扱う」区別をきちんと付けるようにしました。
strlen() や strpos()、preg 系がバイト列のときはバイト単位で動くようにする、といった修正です。
Roundcube 側も、バイト数が重要な箇所を数か所だけ明示的にバイト列として扱うよう直しています。

足りない拡張は作る

PeachPie は主要な拡張を一通り持っていますが、Roundcube が使うものの中には未実装や不完全なものがありました。

  • mbstring: PHP 8.4 が対応する文字コードを全部揃えました。ISO-2022-JP はもちろん、EUC-TW や ArmSCII-8 まで入れています。
  • iconv: .NET に無い文字コードは mbstring 側の実装を使います。
  • ldap: libldap を P/Invoke で呼び出して実装しました。アドレス帳の検索やページングも動きます。
  • pspell: libaspell を P/Invoke で呼び出して実装しました。スペルチェックが使えます。
  • そのほか、bcrypt と PBKDF2、AES-GCM、IntlDateFormatter、Spoofchecker、XMLWriter、SplFixedArray なども追加しました。

コンパイラ本体のバグにもいくつか当たりました。
引数展開(...$args)の型の扱いや、__call に渡される引数が参照のまま渡ってしまう件などです。これらもパッチで直しています。

最終的に、PeachPie へのパッチが32本、Roundcube へのパッチが11本になりました。

本家のユニットテストで答え合わせ

画面で触って動作を確認するのには限界があります。そこで、Roundcube 本家のリポジトリにあるユニットテストを使うことにしました。
PHP 8.4 と PeachPie の両方で同じテストを流し、結果を突き合わせます。

PHPUnit そのものは PeachPie では動かしにくいので、PHPUnit 互換の小さなテストランナーを自作しました。
テストに必要な LDAP サーバーなども、テスト用にコンテナで立てています。

              pass   skip  fail/error
PHP 8.4       1,262  88    2 ※
PeachPie(最初)  —      —     80
PeachPie(最終)  1,264  88    0

※ PHP 側の2件は、テスト環境のロケール設定などによるものです。

最初は80件失敗していました。それを1件ずつ潰して0件にしています。
差分が出たらそのたびに PHP と PeachPie で同じコードを動かして比べる、という地道な作業です。
ユニットテストで見つかった不具合は、画面を触っているだけではまず気づかない種類のものが多かったです。やってよかったと思います。

まだ残っている差

完全に同じというわけではありません。分かっている差は次のとおりです。

  • バイト列由来でも、全部が正しい UTF-8 になってしまった文字列は、strlen() が文字数を返す場合がある
  • ライブラリ内部で出た警告が set_error_handler() に届かない
  • LDAP の SASL バインドは未対応

いずれも、普段使う範囲では影響が出ないことを確認しています。

蛇足

今回の作業は、ほぼ全部 Claude Code と一緒にやりました。
パッチ作成、テストランナーの自作、テストの差分を1件ずつ追う作業は、相当な量の試行錯誤です。人間だけでやっていたら、途中で心が折れていたと思います。笑
こちらは「0件になるまで追い込もう」「テスト用に LDAP コンテナを立ててもいいよ」と方針を出す係でした。

PeachPie 側の修正には汎用的なものも多いので、そのうち本家に還元できるといいですね。

aikawa

PHPでTLS/SSL証明書の有効期間を取得

昔使ったのでメモ用に。

SNI(同じIPアドレスで複数ドメイン)に対応するためオプションを付けています。

他にも色々情報が取れますが、今回は使わないので有効期間のみです。

取り合えずwww.php.netをサンプルに取得してみます。

<?php
$host = 'www.php.net';
$port = 443;
$timeout = 5;
//
$stream_context = stream_context_create(array(
    'ssl' => array(
        'capture_peer_cert' => true,
        'max_redirects' => 0,
        'SNI_enabled'=>true,
        'SNI_server_name'=>$host,
    )
));
$resource = stream_socket_client(
    'ssl://'. $host. ':'. $port,
    $errno,
    $errstr,
    $timeout,
    STREAM_CLIENT_CONNECT,
    $stream_context
);
$cont = stream_context_get_params($resource);
$Data = openssl_x509_parse($cont['options']['ssl']['peer_certificate']);

// span
$tm_start = $Data["validFrom_time_t"];
$tm_end = $Data["validTo_time_t"];
echo date("Y-m-d H:i:s", $tm_start)." - ".date("Y-m-d H:i:s", $tm_end);

出力はこんな感じに。

  • 2021-05-18 19:04:38 – 2022-05-18 19:04:38
murave

Laravel Dusk メモ

Laravel 5.5 & Laradock(https://github.com/laradock/laradock)環境です。
Laradockについては少し古いかもしれません(活発に更新されているのですぐに古くなる)。

Laravel Dusk はブラウザテストをお手軽に!というパッケージです。

https://readouble.com/laravel/5.5/ja/dusk.html

Laradockでのセットアップと使い始めてすぐのログインテストで何回かつまづいたのでメモを残しておきます。

Laradock問題

まず、Laradockのドキュメント通りにセットアップしても正常動作しませんでした(2017/12/13現在)

LaradockでLaravel Duskを動かすメモ(https://qiita.com/uenoryo/items/bdded2dc6f8e5f2075cc)

こちらを参考に動かすことができました。ただし、seleniumコンテナへフォントを入れなければ失敗時などのscreenshotで日本語が表示されないため laradock/selenium/Dockerfile を以下のように編集しました。

FROM selenium/standalone-chrome

MAINTAINER Edmund Luong <edmundvmluong@gmail.com>

RUN sudo apt-get update && sudo apt-get install fonts-ipafont -y

EXPOSE 4444

そしてビルド

$ docker-compose up -d --build selenium
これで豆腐たち(□□□□的な文字化け)とはおさらばです。

Session問題

午前中、Laravelへのブラウザでのログインテストが動かないと悩んでた原因はセッションドライバにarrayを設定してたというオチだったアカウントがこちら。単体テスト用の設定流用してたのが原因(調整漏れ)。
— murave (@murave) 2017年12月13日

.env.dusk.local でDusk用の設定が行えるのですが単体テスト用のものをコピーして修正していたためセッションにarrayドライバを設定してたのでした。

https://readouble.com/laravel/5.5/ja/session.html

array – セッションはPHPの配列として保存されるだけで、リクエスト間で継続しません。

ですよね〜。

他環境への影響を抑えるためにCookieドライバを使用することにしました。

SESSION_DRIVER=cookie
Transaction問題テスト開始時にデータベースクリアしてマイグレーションしてマスター用のSeederでマスター準備してFakerでユーザー作って、みたいにやるじゃないですか。

Laravel 5.5 にはRefreshDatabaseという便利トレイトがあります。

https://readouble.com/laravel/5.5/ja/database-testing.html

前のテストがその後のテストデータに影響しないように、各テストの後にデータベースをリセットできると便利です。インメモリデータベースを使っていても、トラディショナルなデータベースを使用していても、RefreshDatabaseトレイトにより、マイグレーションに最適なアプローチが取れます。テストクラスてこのトレイトを使うだけで、全てが処理されます。

そう、全てが処理されるのです。影響が残らないようにTransaction中に処理してRollbackまでしてくれる。便利。

だが思い出そう。ブラウザテストだということを。

ページ遷移したらデータ消えるわけで、うっかりRefreshDatabaseトレイトを使うと想定通りに動作しません。

そんなわけで、リセットしてマイグレーションまでだけしてくれるDatabaseMigrationsトレイトのほうを使いましょう。

例

<?php
namespace Tests\Browser;
use Tests\DuskTestCase;
use Laravel\Dusk\Browser;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use App\User;

class LoginTest extends DuskTestCase
{
use DatabaseMigrations; //実行時DB内容は消える

public function testLogin()
{
\Artisan::call('db:seed', ['--class' => 'MasterSeeder']);
$user = factory(User::class)->create();

$this->browse(function (Browser $browser) use ($user) {
$browser->visit('/login')
->type('email', $user->email)
->type('password', 'secret')
->press('ログイン')
->assertPathIs('/home');
});
}
}
この例ではDatabaseMigrationsで再構築されたまっさらな状態のデータベースにマスター系投入用のMasterSeederを実行した後にテスト用のユーザーを作成してログイン処理のテストを行っています。

murave

Laravel 5.5 でデータベースdumpをお手軽にとったりもどしたり

muraveです。LTSを使いたい人なのでLaravelのバージョンが5.1から5.5にジャンプアップしました。
その際、いままで使っていたデータベースバックアップのプラグインが使えなくなりました。

データベースdumpをお手軽にとったりもどしたりしたかっただけだったんだけど、重厚なバックアップ用パッケージが見つかった。これはこれで入れとくかぁ。
— murave (@murave) 2017年11月24日

ということで

https://github.com/spatie/laravel-backup

を導入。良い感じです。もしかして?と、この方のリポジトリを探してみたらありました。

https://github.com/spatie/laravel-db-snapshots

This package provides Artisan commands to quickly dump and load databases in a Laravel application.

そうそう、この記事の対象データベースはPostgreSQLです。

PostgreSQLの場合にレストア時にエラーでデータベースを飛ばしたので、その対処方法のメモだったりします。MySQL、SQLiteにも対応らしいですが試してません。導入や使い方はドキュメント見てくださいね。

ドキュメント通りにスナップショットをロード(restore)しようとしたらエラーでコケたのでした。

# artisan snapshot:load my-first-dump

[Illuminate\Database\QueryException]
SQLSTATE[42601]: Syntax error: 7 ERROR: syntax error at or near "\"
LINE 464: \.
^ (SQL: --

### 以下略

で、コケるとデータベース飛びます(掃除がリストアの前に別途行われるようです)。

バックアップ・リストアのテストしててバックアップオプションを変えてからデータのバックアップが取れないと思ってたら、リストアのテストの失敗でデータが消滅してたせいだった。
— murave (@murave) 2017年11月27日

こんあとき、ボクのように上書きで再度スナップショットを取ると空のデータベースのdumpで上書きという悲劇が起こるので気をつけましょう。

dumpファイルの内容を調べるとCOPY from stdinで.から読むようになってるところでコケてます。

「ははーん。pg_dump呼び出しに–insertsつければいいんだな」

pg_dumpに–insertsオプションをつけるとデータをCOPYではなくINSERTコマンドでdumpします。

「どうやってつけたらいいんだ?」

コードを追ったらdatabase.phpでdumpのオプション付けられるようになってました。
insertsオプションにも対応してます。サンプル載せときます。

'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public',
'sslmode' => 'prefer',
//PDO絡みで環境によって型がばらつく可能性があるので割り切って全て文字列で取得。
'options' => [
PDO::ATTR_STRINGIFY_FETCHES => true,
],
'dump' => [
'useInserts',
],
],
‘dump’ => [‘useInserts’] の所です。無事snapshot:loadできるようになりました。

# artisan snapshot:create for-test-dump
# artisan --env=testing snapshot:load for-test-dump
これはデフォルトの環境でスナップショットを取ってtesting環境でロード(restore)してます。

PDO::ATTR_STRINGIFY_FETCHESについての余談

前記database.phpのサンプルのコメントにありますが、開発環境ではintで取れてたのに本番のサーバーにデプロイしたらstringで返ってくるなどデータ取得時に動作環境によって型がばらつくことがあります。

厳密比較の動作が異なって判定が変わったり、unit testがコケたりと困ったことになるので全て文字列で取得するオプションを設定するようにしています。

Model経由で値を扱う場合は

/**
* キャスト設定(文字列以外は要設定)
*
* @var array
*/
protected $casts = [
'id' => 'integer',
'updated_by' => 'integer',
'options' => 'array', //jsonb
];
のように$castsに書いておけばLaravelさんがキャストしてくれます。

okada

dockerからAmazon EC2への奮闘記

今までdockerを使ったことがなかった私、勉強がてらLaradockを使ってLaravelで開発を行っていました
そこからAmazon EC2へ移行するまでの過程で色々行き詰まり、
なんとかLaradockからEC2へ環境移行することができましたので、
今後のメモのためにログを残しておこうと思います

今回書く内容はLaradockの環境設定〜Amazon EC2へ設置するまでの簡単な流れで行き詰ったものを晒すだけのほぼ日記の様なものです(笑)

Docker for Mac 〜 Laradockを導入するまで

$ git clone --depth=1 -b v5.5.1 https://github.com/LaraDock/laradock.git
$ cd laradock
$ cp env-example .env
$ vi .env
…とこのようにまずはドキュメントに従って導入を行っていました
しかし

問題1:Laradockを導入したが起動しない

$ docker-compose up -d nginx mysql
ERROR: Couldn't connect to Docker daemon - you might need to run `docker-machine start default`.
$ docker-machine start default
Host does not exist: "default"
defaultを設定しないといけない?

$ docker-machine create --driver virtualbox default
をすればよい、とのことだったので実行してみたけれどエラー発生
そもそもDocker for Macはvirtualbox使わなくても良くなった、との記述を見かけた気が…

解決:boot2dockerとDocker for macが混在していた

Laradockの問題ではなく、dockerの問題でした
どうやらDocker for Macを入れる前にboot2dockerをhomebrewで入れていたようで、
PC内にdockerが2つある状態でした
ということでboot2dokerを削除

brew uninstall docker boot2docker docker-compose docker-machine
念のためDocker for Macもアンインストールして入れ直し、もう一度動かしてみた所無事にLaradockを動かすことができました!
まぁこんなことしでかすなんて自分くらいしかいないと思うので参考にはならないかもしれませんが、思い出の一つとして…

問題2:MySQLが動かない

次のようなエラーが発生してMySQLが起動しない
docker-compose psを実行してみた所、laradock_mysql_1のstateはExitでした

$ docker-compose up --build -d nginx mysql
$ docker-compose exec mysql bash
ERROR: No container found for mysql_1
ログを確認

$ docker logs laradock_mysql_1
2017-08-01T08:01:50.339823Z 0 [ERROR] [FATAL] InnoDB: Table flags are 0 in the data dictionary but the flags in file ./ibdata1 are 0x4800!
解決:MySQLのデータファイルを削除このエラーに関しては.envファイルを確認し、MySQLのデータファイルを削除した所、起動に成功しました

$ vi .env ⇐ laradock/.envを確認
10 ### Data Path:
11 # For all storage systems.
12
13 DATA_SAVE_PATH=~/.laradock/data
$ cd ~/.laradock/data
$ mv data data.old
Laradockが動かない…動かない…と入れ直しを繰り返した時にゴミファイルとして残ってしまっていた模様

ローカルからAmazon EC2へ

システムが出来上がり、次はシステムをローカルからEC2へ移行作業へ…
まずLaradock環境内のバージョンを確認
EC2側にもPHP7.1、MySQL5.7、nginxをインストール

$ docker-compose exec workspace bash
# php -v
PHP 7.1.4-1+deb.sury.org~xenial+1 (cli) (built: Apr 11 2017 22:12:32) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
with Zend OPcache v7.1.4-1+deb.sury.org~xenial+1, Copyright (c) 1999-2017, by Zend Technologies
# php artisan --version
Laravel Framework 5.4.30
# exit
exit

$ docker-compose exec mysql bash
# mysql --version
mysql Ver 14.14 Distrib 5.7.19, for Linux (x86_64) using EditLine wrapper
# exit
exit
EC2側環境作成PHP、MySQL、nginxインストール後…
以下のファイルを編集

$ vi /etc/php-fpm.d/www.conf
24 user = nginx
25 ; RPM: Keep a group allowed to write in log dir.
26 group = nginx

48 listen.owner = nginx
49 listen.group = nginx
50 listen.mode = 0660
$ vi /etc/nginx/nginx.conf
40 server {

44 root /var/www/html/public;

73 location ~ \.php$ {
74 root /var/www/html/public;
75 fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
76 fastcgi_index index.php;
77 fastcgi_param SCRIPT_FILENAME /var/www/html/public$fastcgi_script_name;
78 include fastcgi_params;
79 }
それぞれのサービスを起動 & 自動起動するように設定

$ sudo service nginx start ⇐ サービス起動
$ sudo service php-fpm start
$ sudo service mysqld start
$ sudo chkconfig nginx on ⇐ 自動起動
$ sudo chkconfig php-fpm on
$ sudo chkconfig mysqld on
MySQL5.7ではrootユーザには初期パスワードなるものが存在し、
最初はそのパスワードでログインする

[ec2-user@ip-172-31-46-167 ~]$ sudo cat /var/log/mysqld.log
2017-08-03T06:59:34.488345Z 1 [Note] A temporary password is generated for root@localhost: Jt?eJUpbO6kl ⇐コレ
rootのパスワードは変更しておくべし

$ mysql -u root -p
> ALTER USER 'root'@'localhost' IDENTIFIED BY 'パスワード';
Laravelのドキュメントに従いComposerをインストール後、Laravel5.4を導入

$ composer global require "laravel/installer"
$ cd /var/www/
$ laravel new html
ブラウザでLaravelのTOP画面が表示されることを確認

Laradockからお引っ越し

ローカルのLaravelより、gitを使ってアーカイブファイル作成!
Laradockには既に.gitignoreファイルが準備されており、logファイル等など必要ないファイルは省かれる様になっていました。便利!

git archive HEAD --output=~/WorkSpace/project/release/archive.zip
このファイルを/var/www/htmlに上書き(.envの取扱い注意!)し、
ファイルの権限を変更 chmod o+w -R storage
そして以下のコードを実行

$ php artisan config:cache
$ php artisan route:cache
.env.exampleを.envに書き換え、EC2上の環境に合わせた設定に書き換えます
そしてブラウザで確認!

The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths.

こんなエラーが表示されましたよ…
以下を実行

$ php artisan key:generate
$ php artisan config:clear
.envファイルの APP_KEY= にキーがセットされるわけですね
無事TOPが表示されました!

でもページが遷移しませんよ?

/loginへ遷移できません。404エラーです。
原因はnginxの設定。5.4のドキュメント通りにnginx.confを編集。

$ vi /etc/nginx/nginx.conf
49 location / {
50 try_files $uri /index.php?$query_string;
51 }
これで他のページへも遷移できるようになりました!
以上がEC2への引っ越し奮闘記になります。

murave

Laravel 5.1 でログ出力、faultline-php追加編

Laravel の次のLTS、5.5もそろそろ来そうな昨今ですが、また5.1のログ出力の話です。

昨日のBlogに書いたようにプロジェクトのログ出力に faultline-php (https://github.com/faultline/faultline-php) を組み込みました。Monolog integration のありがたさよ。

今日は faultline 関係の .env 対応を進めているのですが、コードがややこしくなる前の素朴なのをサンプルとして出しておこうかな〜と。

組み込み前の状態についてはこちらをご参照ください。

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        //登録されたloggerのhandlerを置き換える
        $monolog = \Log::getMonolog();
        $monolog->popHandler(); //既存のhandlerを削除
        $monolog->pushHandler(
                    (new \Monolog\Handler\RotatingFileHandler(
                        $this->app->storagePath() . '/logs/' 
                            . $this->app->environment() . '_'
                            . (defined('ARTISAN_BINARY') ? ARTISAN_BINARY . '_' : '')
                            . posix_getpwuid(posix_geteuid())['name'] 
                            . '.log',
                        \Config::get('app.log_max_files', 5),
                        (\Config::get('app.debug') || $this->app->environment('testing')) 
                            ? \Monolog\Logger::DEBUG 
                            : \Monolog\Logger::INFO //出力レベル制御のため追加
                    ))
                    ->setFormatter(new \Monolog\Formatter\LineFormatter(null, null, true))
                );
        //Faultline\MonologHandlerを追加。Notifierはregister()で行っているバインドから取得。
        $monolog->pushHandler(new \Faultline\MonologHandler(App('faultline.notifier')));
        $monolog->pushProcessor(
                    new \Monolog\Processor\IntrospectionProcessor(
                        \Monolog\Logger::NOTICE,    //DEBUG,INFOは発生場所の情報を付加しない
                        [
                            'Illuminate\\',
                            //他、呼び出し元としてはスキップしたい対象を設定,
                        ]
                    )
                );

        //.envのAPP_QUERY_LOGGINGがtrueの場合クエリを独自ログquery_{実行アカウント}.logに出力する。
        if(env('APP_QUERY_LOGGING', false)) {
            \DB::listen(function($query, $bindings, $time){
                $queryLogger = (new \Monolog\Logger(
                    $this->app->environment()
                ))->pushHandler(
                    new \Monolog\Handler\StreamHandler (
                        $this->app->storagePath() . '/logs/query_' . posix_getpwuid(posix_geteuid())['name'] . '.log')
                );

                $queryLogger->info('QUERY:[' . $query . '] BINDINGS:'
                        . json_encode($bindings), ['TIME' => $time]);
            });
        }
    }

    public function register()
    {
        //faultline.notifier バインド
        $this->app->bind('faultline.notifier', function($app) {
            return new \Faultline\Notifier([
                'project' => "project_name_{$app->env}",
                'apiKey' => 'XXxxXXXXxxxXXXxxxxXXXxxxXXX',
                'endpoint' => 'https://xXXXXxxxX.execute-api.ap-northeast-1.amazonaws.com/v0',
                'notifications' => [
                    [
                        'type'=> 'slack',
                        'endpoint'=> 'https://hooks.slack.com/services/XXxxxxXXXxxxXXX/XXxxXXxXXXxxxxXXXxxxXXX',
                        'channel'=> '#faultline',
                        'username'=> 'faultline-notify',
                        'notifyInterval'=> 5,
                        'threshold'=> 10,
                        'timezone'=> 'Asia/Tokyo'
                    ],
                ]
            ]);
        });
    }
}
murave

PHPカンファレンス福岡2017 に参加しました

2017年6月10日(土)に開催された PHPカンファレンス福岡2017 に参加しました。

http://phpcon.fukuoka.jp

今回は日帰りだったので本編のみの参加。懇親会は残念ながら未参加でした。
懇親会LT聞きたかった。

以下の流れで参加しました。

  • 開会式
  • PHP7で堅牢なコードを書く – 例外処理、表明プログラミング、契約による設計 (和田卓人 @t_wada)
  • 新卒2年目がサービス開発の際に乗り越えた課題とその解法など (株式会社Fusic 濱野泰明 @gorogoroyasu)
  • 実践Action Domain Responder (竹澤有貴 @ex_takezawa)
  • 僕達がやってきたレガシープロジェクトとの付き合い方 (株式会社ハシゴ 渡辺 謙一郎 @_nabeen)
  • 海外からのアクセスを制御する (GMOペパボ株式会社 田平 康朗)
  • faultline / faultline-php によるサーバ管理不要なエラートラッキングとその効果 (小山健一郎 @k1LoW)
  • Dockerを本番で使ってみて分かったこと (富所亮 @hanhan1978)
  • PHP-MLを使用して手軽に機械学習にトライしてみる (柴山嶺 @serima)
  • 僕たちエンジニアにとってのPHPときどき機械学習 (立石賢吾 @tkengo)
  • ライトニングトーク
  • 閉会式

事前に決めていた今回の私のメインターゲットは『Dockerを本番で使ってみて分かったこと』

なぜかというと本番で使う(もしくは使っている)から。

このセッションで「本番に使ってる人〜?」の問いかけに一人だけ手を上あげたの、私でした。
微妙なタイミングだったのであげるかどうか迷ったんですが。

開発状況を顧客に見てもらうためにつくったDocker環境を今週の月曜からそのまま本番運用に投入、リリースするって状況だったのです。「懇親会で語り合いましょう」と言っていただいたんですが

懇親会は今回参加しないんだよなぁと。あと運用レベルが違いすぎる(^_^;)
— murave (@murave) 2017年6月10日

リリースしたサービスを汎用化してコンテナベースでデプロイして提供ってのをやりたいなぁと思っているので、本当、参考になりました。

それから『faultline / faultline-php によるサーバ管理不要なエラートラッキングとその効果』を聞いて「使いたい!」と思いました。

あと、個人的に印象にのこってるのは『海外からのアクセスを制御する』(mrubyですけど)とLTの『WHERE 1 =1』(ホラー的に)でしょうか。

今年もよい刺激をいただきました。いい言葉を沢山聞いたなぁ。

ありがとうございました。

余談

その1

去年は「PHP7未満無視で書きたいなぁ」なんて言ってましたが、もうすっかりPHP7ですね。

PHP7は良いものだ #phpconfuk #phpconfuk_fusic— murave (@murave) 2017年6月10日

タイプヒンティング大好き\(^o^)/

その2

今年は翌月曜日がリリース予定だったりで余裕がなく、片道3時間超の道程を自走での日帰り参加となりました。疲れが溜まっていたのかサービスリリース後から体調をがっつり崩してしまいまして、もっと余裕をもって行動したいものだと思いました(懇親会も参加できなかったし)

自走したのがあかんかったかなぁとは思ってる。

murave

Laravel 5.1 でログ出力、それから (ログメールドライバー動作不良対処、クエリ出力の調整)

おひさしぶりです。運用していたら結構変わったので、またこのネタです。

一応以前の記事へのリンクおいときます。

『Laravel 5.1 で実行ユーザー毎にログ出力』

『Laravel 5.1 でログ出力、その後 (出力レベル設定、クエリ出力)』

が、以前の記事の方法だとログメールドライバーが動作しないという問題が発生したので参考にしちゃだめ。

というわけで、以下の要件と不具合対処のために改修した内容を紹介します。

  • ログファイルの権限絡みのエラー回避、何処から起動された処理かわかりやすくする等の目的で実行ユーザー名ベースでログファイルを作成。
  • 動作環境(local, testing, production等のenvironment)でもログファイルを分ける。
  • バッチ処理やartisanのコマンド実行の場合はARTISAN_BINARYをログファイル名に挿入してWebアクセスとはログファイルを分ける。
  • デバッグフラグ(app.debug)を落とした状態ではINFO以上のログのみ出力。
  • .envでAPP_QUERY_LOGGINGにtrueを設定したら発行されたクエリをinfoログで専用ログファイルに出力(デバッグフラグでのコントロールを廃止し個別にコントロール可能とし、ファイルも分けた)
  • 以前の実装ではログメールドライバー(メール送信内容をログ出力するドライバ)が正常動作しなかったためLoggerインスタンスを差し替える方法から既存Loggerインスタンスのhandlerを差し替える方法に実装を変更。

class AppServiceProvider extends ServiceProvider
{
public function boot()
{
//登録されたloggerのhandlerを置き換える
$monolog = \Log::getMonolog();
$monolog->popHandler(); //既存のhandlerを削除
$monolog->pushHandler(
(new \Monolog\Handler\RotatingFileHandler(
$this->app->storagePath() . '/logs/'
. $this->app->environment() . '_'
. (defined('ARTISAN_BINARY') ? ARTISAN_BINARY . '_' : '')
. posix_getpwuid(posix_geteuid())['name']
. '.log',
\Config::get('app.log_max_files', 5),
(\Config::get('app.debug') || $this->app->environment('testing'))
? \Monolog\Logger::DEBUG
: \Monolog\Logger::INFO //出力レベル制御のため追加
))
->setFormatter(new \Monolog\Formatter\LineFormatter(null, null, true))
);
$monolog->pushProcessor(
new \Monolog\Processor\IntrospectionProcessor(
\Monolog\Logger::NOTICE, //DEBUG,INFOは発生場所の情報を付加しない
[
'Illuminate\\',
//他、呼び出し元としてはスキップしたい対象を設定,
]
)
);

//.envのAPP_QUERY_LOGGINGがtrueの場合クエリを独自ログquery_{実行アカウント}.logに出力する。
if(env('APP_QUERY_LOGGING', false)) {
\DB::listen(function($query, $bindings, $time){
$queryLogger = (new \Monolog\Logger(
$this->app->environment()
))->pushHandler(
new \Monolog\Handler\StreamHandler (
$this->app->storagePath() . '/logs/query_' . posix_getpwuid(posix_geteuid())['name'] . '.log')
);

$queryLogger->info('QUERY:[' . $query . '] BINDINGS:'
. json_encode($bindings), ['TIME' => $time]);
});
}
// 以下略
クエリ出力については他のログに混ざると見にくくなる、デバッグフラグに連動させていると本番環境での調査で使えない、クエリ出力のログファイルは日付でローテーションさせるより同じファイル名のログファイル一本に出力されるほうが扱いやすい(消して良いものだし)、ということで今の形におさまりました。

ログファイル名に関して、例えばnginxユーザーがphpunitを回したときなど testing_artisan_nginx-2017-01-31.log みたいになるので「なげぇな〜」とは思いますが、場合によってログファイルが別れているのは便利ですよ。

murave

Laravel 5.1 でログ出力、その後 (出力レベル設定、クエリ出力)

2017/2/1
不具合があったので『Laravel 5.1 でログ出力、それから (ログメールドライバー動作不良対処、クエリ出力の調整)』のほうを見て下さい

少し前に書いた『Laravel 5.1 で実行ユーザー毎にログ出力』という記事の続き的なものです。
環境も変わらず Laravel Framework version 5.1.35 (LTS) です。

前回のあらすじ
「ログ出力時に実行ユーザー名ベースでログファイルを作るようにすればログファイルの権限絡みのエラーを回避できるし何処から起動された処理かわかりやすくなって良くない?」と思ったボク。AppServiceProviderのboot()でloggerを差し替えつつも「ここでやっちゃっていいのかしら」という戸惑いを感じたりもして…


で、戸惑ってましたが実行準備の一貫としてAppServiceProviderでやってて問題なさそうかなと思ってます。ツッコミもこなかったし。

今回は実際に開発で使っていて

・デバッグフラグ(app.debug)を落とした状態ではINFO以上のログのみ出力
・デバッグフラグ(app.debug)を立てた状態では発行されたクエリをDEBUGログで出力

という機能追加をしたので、その紹介です。

class AppServiceProvider extends ServiceProvider
{
public function boot()
{
$this->app->instance('log', new \Illuminate\Log\Writer(
(new \Monolog\Logger(
$this->app->environment()
))->pushProcessor(
new \Monolog\Processor\IntrospectionProcessor(
\Monolog\Logger::DEBUG,
[
'Monolog\\',
'Illuminate\\',
//他、呼び出し元としてはスキップしたい対象を設定,
]
)
)->pushHandler(
new \Monolog\Handler\RotatingFileHandler(
$this->app->storagePath() . '/logs/' . posix_getpwuid(posix_geteuid())['name'] . '.log',
\Config::get('app.log_max_files', 5),
\Config::get('app.debug') ? \Monolog\Logger::DEBUG : \Monolog\Logger::INFO //出力レベル制御のため追加
)
)
)
);

//app.debugがtrueの場合クエリをログに出力する。
if(\Config::get('app.debug')) {
\DB::listen(function($query, $bindings, $time){
\Log::debug('QUERY:[' . $query . '] BINDINGS:'
. json_encode($bindings), ['TIME' => $time]);
});
}
}
// 以下略

出力レベルはロギングハンドラ側に設定があったんですね。前回は未指定だったので全レベルが出力される状態でした。
app.debugでコントロールすることにして3項演算子でapp.debugが有効な場合はDEBUG、無効な場合はINFOを設定することにしました。

もう一つのクエリ出力についてはloggerとは別の話ですが、設定した場所も近いですしついでに紹介。
実行されたクエリを見ないと不安になるので確認方法は必須ですが、デバッグ状態で常に出力されるのは過剰な感じもしています。見えないよりはいいかな。

murave

Laravel 5.1 で実行ユーザー毎にログ出力

2017/2/1
不具合があったので『Laravel 5.1 でログ出力、それから (ログメールドライバー動作不良対処、クエリ出力の調整)』のほうを見て下さい

Laravel Framework version 5.1.35 (LTS) についての記事です。

muraveです。あんまりにもあんまりな記事(「PHP カンファレンス福岡 2016 で思ったこと」)を上げて反省しましたのでPHPお役立ち記事を書きたいと思います。

CLIコマンドを作ることができるフレームワーク、CakePHPなどもですが、ログ出力時にWebとCLIで実行したユーザーが異なり、権限がらみのエラーが発生したりしがちです。

そもそもログを調べる時、WebとCLIは別ファイルになってた方が良くないです?

ということでログファイルを分ける方法を考えていたのですが、実行ユーザー名でログファイルを作ればいいと気がつきました。まぁ、nginxやapacheユーザーでCLIを扱うと混ざっちゃいますが。

よ~し、おっちゃん、実行ユーザー名からファイル名を決めるインスタンスを作ってloggerを差し替えちゃぞ~

ついでにextraに呼び出し元情報を追加し(参考:「Laravelのログにファイル名やメソッド名を出力する」)、日毎のファイル作成(RotatingFileHandlerベース)にしましょう。

class AppServiceProvider extends ServiceProvider
{
public function boot()
{
$this->app->instance('log', new \Illuminate\Log\Writer(
(new \Monolog\Logger(
$this->app->environment()
))->pushProcessor(
new \Monolog\Processor\IntrospectionProcessor(
\Monolog\Logger::DEBUG,
[
'Monolog\\',
'Illuminate\\',
//他、呼び出し元としてはスキップしたい対象を設定,
]
)
)->pushHandler(
new \Monolog\Handler\RotatingFileHandler(
$this->app->storagePath() . '/logs/' . posix_getpwuid(posix_geteuid())['name'] . '.log',
\Config::get('app.log_max_files', 5)
)
)
)
);
}
// 以下略

bootstrapのapp.php内で設定しようとしたらenv関係でエラーが出たので上記のようにAppServiceProviderのboot()で差し替えていますが、もっと適切な場所があるのでは?というのが気になるところ。

もっとスマートな方法があったら助言いただけるとうれしいなぁ。


2016/07/20 『Laravel 5.1 でログ出力、その後 (出力レベル設定、クエリ出力)』というその後の機能拡張の話を書きました。