ぷろじぇくと、みすじら。

MySQLへのクエリをApplication Insights(.NET)のDependencyに出したい

Created at:

Application InsightsにはDependencyテレメトリーというリクエスト中に発行された外部リソースへのアクセスなどを記録する仕組みがあり、.NET向けの一式を入れておけばHTTPリクエストやSQL Serverへの問い合わせが自動で記録されます。

一方、標準で対応してないものをDependencyに出すには自前で何らかの方法で記録してあげる必要があります。MySQL Connector/Netもその例にもれず自動では記録されません。SQL Serverへの問い合わせが記録されるのはSqlClient (SQL Serverクライアント)のイベントを記録しているからであって、それ以外のデータベースドライバーでは記録されないのです。

というわけで、当然MySQLへの問い合わせでもDependencyに表示されてほしくなります。

Dependencyとしての記録

まずはそもそもApplication Insights上でDependencyとして記録するにはどうすればいいのかというところからです。

ドキュメントを見るとDependencyはテレメトリーの一種で、DependencyTelemetry というレコードを記録すればよいということになっています。

記録するには DependencyTelemetry を生成する方法と、TrackDependencyメソッドで記録する方法があり、今回は DependencyTelemetry を使った記録方法で実装してみます。後者は細かいことはできないもののメソッド呼び出し一発とお手軽です。

実際の手順としては次のようになります。

これをコードにするとこうなります。

// TelemetryClientはスレッドセーフなので使いまわせる
var telemetryClient = new TelemetryClient();

using (var operation = telemetryClient.StartOperation<DependencyTelemetry>("DependencyName"))
{
    var telemetry = operation.Telemetry;
    telemetry.Type = "ResourceType"; // Dependencyの種類(Http, SQLなど)
    telemetry.Target = "リクエスト送信先"; // エンドポイントのホスト名とか
    telemetry.Data = "何か生データー"; // SQLとか

    // 何か時間のかかる処理...
    await Task.Delay(1000 * 3);
}

難しいことはないですね。これでApplication Insightsに記録する方法はなんとなく理解できました。

MySQLの呼び出しを記録する

Dependencyとして記録する方法がわかったので次はMySQLへの問い合わせを記録する方法です。

今回はMySQLドライバーはMySQL公式のConnector/Netを利用していて、単純にSQLのクエリを記録したいと考えていますが、それにはそのクエリのタイミングをつかむ必要があります。そこで少し調べてみると、Connector/NetにはInterceptorというExecute系メソッドに割り込んでSQLのロギングなどが行える仕組みがあったのでそれを利用します。

InterceptorにはあらかじめExecute系メソッドに割り込むためのベースクラスである CommandInterceptorBase クラスがあるので、このクラスを継承して各種メソッドをオーバーライドします。そしてここではオーバーライドしたメソッドでDependencyの記録を行えば良さそうというわけです。

実際に実装した例はこんな感じになります。DependencyのTargetやNameといった値の設定はSQL Serverでの記録と同じような形にしました。

using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using MySql.Data.MySqlClient;

namespace WebApplication2.Diagnostics
{
    public class ApplicationInsightsBaseCommandInterceptor : BaseCommandInterceptor
    {
        private TelemetryClient _telemetryClient = new TelemetryClient();
        private string _name;

        public override bool ExecuteNonQuery(string sql, ref int returnValue)
        {
            using (var operation = _telemetryClient.StartOperation<DependencyTelemetry>(_name))
            {
                var telemetry = operation.Telemetry;
                telemetry.Type = "SQL";
                telemetry.Data = sql;
                telemetry.Target = _name;

                return base.ExecuteNonQuery(sql, ref returnValue);
            }
        }

        public override bool ExecuteReader(string sql, CommandBehavior behavior, ref MySqlDataReader returnValue)
        {
            using (var operation = _telemetryClient.StartOperation<DependencyTelemetry>(_name))
            {
                var telemetry = operation.Telemetry;
                telemetry.Type = "SQL";
                telemetry.Data = sql;
                telemetry.Target = _name;

                return base.ExecuteReader(sql, behavior, ref returnValue);
            }
        }

        public override bool ExecuteScalar(string sql, ref object returnValue)
        {
            using (var operation = _telemetryClient.StartOperation<DependencyTelemetry>(_name))
            {
                var telemetry = operation.Telemetry;
                telemetry.Type = "SQL";
                telemetry.Data = sql;
                telemetry.Target = _name;

                return base.ExecuteScalar(sql, ref returnValue);
            }
        }

        public override void Init(MySqlConnection connection)
        {
            _name = String.Format("{0} | {1}", connection.DataSource, connection.Database);

            base.Init(connection);
        }
    }
}

Interceptorを実装したら最後にアプリケーションの設定でデータベース接続文字列に commandinterceptors パラメータを追加して読み込ませます。パラメータの値は<CommandInterceptorClass>,<Assembly>というフォーマットで、次のようなものになります。

commandinterceptors=WebApplication2.Diagnostics.ApplicationInsightsBaseCommandInterceptor,WebApplication2

そしてこれを有効にした状態でアプリケーションを実行するとApplication Insightsに記録されます。もちろんAzureに接続していない場合でもVisual Studioで確認できます。

この例では問い合わせが1ms以下なので全く面白くなくて残念ですが、ともあれこれで取れるようになったので無いよりは全然よさそうです。

AuthenticationHandlerで処理する方法をアクションごとに変更する

Created at:

ASP.NET Core MVCでは認証は基本的にフィルターではなく、認証ハンドラーを認証スキーム名で登録しておき、使用したい認証スキームをAuthorize属性などで指定するという形になっています(デフォルトという指定もできます)。

今回やりたいこととしては「認証機構は基本的に一つで、あるアクションの時だけ認証中に追加で特殊な処理を行いたい」というパターンです。例えばコントローラーに共通のAuthorizeを付けているが、一部特殊なアクションでは追加の処理をしたいというケースです。

難しいところ

そもそも認証ハンドラーはASP.NET Coreの一部であってMVCとは切り離されたものなので、操作可能なものは主にHttpContextとなります。つまりASP.NET Core MVCが認識しているアクションであるとかコントローラーであるとかを取り出すことは難しいため、アクションに付けた属性を引いてくるといったことはできないという悩みがあります。

うまくいかないパターン

そこで最初に考えたのは、二つの認証スキームとして登録してそれぞれで利用する認証スキームを変更するという方法です。

まず次のようなオプションを持つAuthenticationHandlerを用意します。

public class CustomAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
    public bool EnableNanika { get; set; }
}

public class CustomAuthenticationHandler : AuthenticationHandler<CustomAuthenticationSchemeOptions>
{
    public CustomAuthenticationHandler(IOptionsMonitor<CustomAuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (Options.EnableNanika)
        {
            // 一部で必要な特殊な処理...
        }
        return Task.FromResult(AuthenticateResult.Fail("Unauthorized"));
    }

    protected override Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Response.Headers["WWW-Authenticate"] = "Nanika";
        return base.HandleChallengeAsync(properties);
    }
}

このハンドラーをStartupで二つの認証スキームとして登録します。

services.AddAuthentication()
    .AddScheme<CustomAuthenticationSchemeOptions, CustomAuthenticationHandler>("Custom1", options =>
    {
        options.EnableNanika = false;
    })
    .AddScheme<CustomAuthenticationSchemeOptions, CustomAuthenticationHandler>("Custom2", options =>
    {
        options.EnableNanika = true;
    });

コントローラーにAuthorize属性を付け、その際にコントローラーとアクションで認証スキームを別々に設定します。

[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "Custom1")]
public class ValuesController : Controller
{
    // GET api/values
    [HttpGet]
    [Authorize(AuthenticationSchemes = "Custom2")]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }
}

これでどうかと思ったのですが、この指定で実際に動かすとコントローラーの指定の方が勝つことになります。当然ですがコントローラーにAuthorizeを付けずにアクションごと個別につけると期待通りに動作します。

うまくいかないパターン(2)

認証はフィルターとしても実装できるので(Authorizeも実体はMVCのフィルターで、中で認証ハンドラーを呼び出している)、それをかけてみるのはどうかという風になります。

public class CustomAuthorize : Attribute, IAsyncAuthorizationFilter
{
    public bool EnableNanika { get; set; }

    public Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        // ここで認証処理をあれこれやる
        return Task.CompletedTask;
    }
}

この場合にはコントローラーとアクション両方に指定すると二つのフィルターが呼ばれることになる(=二回処理が走る)ので都合が悪い感じになります。それに加えてAuthorize属性と同じようなことを書いてあげる必要があり面倒です。

解決策: フィルターを組み合わせるパターン

認証はフィルターとして実装できるという点を利用して、認証自体は行わずリクエストのデータにフラグを立てておき、認証ハンドラーで取り出して何とかする方法があります。その方法であれば 自前のフィルター(ASP.NET Core MVC) → 自前の認証ハンドラー (ASP.NET Core) という形にできます。

まずアクションにつけるフィルターを作ります。

public class UseCustomAuthenticationWithNanikaAttribute : Attribute, IAsyncAuthorizationFilter, IOrderedFilter
{
    public int Order => int.MinValue;

    public Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        context.HttpContext.Features.Set<IUseCustomAuthenticationWithNanikaFeature>(new UseCustomAuthenticationWithNanikaFeature
        {
            Enable = true
        });
        return Task.CompletedTask;
    }
}

public interface IUseCustomAuthenticationWithNanikaFeature
{
    bool Enable { get; set; }
}
public class UseCustomAuthenticationWithNanikaFeature : IUseCustomAuthenticationWithNanikaFeature
{
    public bool Enable { get; set; }
}

このフィルターはOnAuthorizationAsyncでHttpContextのFeatures(リクエスト単位の機能を入れておけるもの)にフラグ情報を持つFeatureを保存します。IOrderedFilterを実装しているのはAuthorize属性より先に来てほしいためです。

次に認証ハンドラーを作ります。このハンドラーの中でFeaturesから先ほどのフィルターでつけていたFeatureを引っ張り出します。

public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public CustomAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var enableNanika = Context.Features.Get<IUseCustomAuthenticationWithNanikaFeature>()?.Enable ?? false;
        if (enableNanika)
        {
            // 一部で必要な特殊な処理...
        }

        return Task.FromResult(AuthenticateResult.Fail("Unauthorized"));
    }

    protected override Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Response.Headers["WWW-Authenticate"] = "Nanika";
        return base.HandleChallengeAsync(properties);
    }
}

Startupで認証ハンドラーを登録します。この際、AddAuthenticationでデフォルト認証スキームを登録しないように注意します。デフォルトの認証スキームとなるハンドラーは順番が特別なことになるので意図した挙動になりません。

services.AddAuthentication()
    .AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>("Custom", options => { });

そしてコントローラーにはAuthorize属性を、特別な処理をしたいアクションにはUseCustomAuthenticationWithNanika属性でフィルターを付けます。

[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "Custom")]
public class ValuesController : Controller
{
    // GET api/values
    [HttpGet]
    [UseCustomAuthenticationWithNanika]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

    // GET api/values/5
    [HttpGet("{id}")]
    public string Get(int id)
    {
        return "value";
    }
}

これでアクションに属性を指定するだけで認証時の挙動を少し変更するということが可能になります。

U-NEXT TV (第二世代)はAndroidおもちゃではない

Created at:

U-NEXT、9,800円のHDR対応Android TV「U-NEXT TV」。カラオケやYouTubeも - AV Watchという記事を見て「先着100名で、新規にU-NEXT登録した人を対象にU-NEXT TVをプレゼントするキャンペーン」とあったので登録してみたところ無事届きました。というわけでAndroidとして遊べないかなというところに関してのお話です。

製品仕様はキャンペーンサイトに書いてあり、Android 7.0であることが明記されています。

U-NEXT TV (2nd Gen)

端末自体はHuaweiです。この手のセットトップボックスではHuawei製はよくあるようです(たしかauやdocomoのものもそうだったはず)。

OS

さて、Androidに関してですがよく見ると公式サイトの製品仕様にはAndroid 7.0とはあるもののAndroid TVとは書いていないのでもしかしたら…と思っていたのですがセットアップし始めるとやはりAndroid TVではなく単なるAndroidです。

そもそもセットアップステップがAndroid TV風でなくU-NEXTのログインが必須であったり、オンスクリーンキーボードもAndroid TVのそれではない、ホーム画面は独自というあたりでお察しです。

製品仕様によればアプリも利用できることになっているのですがAndroidがベースになっているだけでGoogleサービスが入っていないのでGoogle Playは使えません。「U-NEXTサービス、HDMI-CEC対応、Player for YouTube、Androidなど」とあるので使えるのかと思いきや。

YouTubeを観れそうな雰囲気ですがGoogle Play入ってないのにどうやって?と思ったらHuaweiの独自アプリでした。oh…。

現状U-NEXT用のアプリストアがあるわけでもないのでプレインストールのアプリのみ利用できます。なお設定のアプリ管理からアプリをアンインストールできるのですが、それらはインストールする口がないので消したら最後です(これはひどい。

参考までにほぼ同型機のdocomoのドコモテレビターミナルは説明書を見てもわかるのですが正真正銘のAndroid TVでロゴも表記されています。

adbを使う

とはいえAndroidなのでUSBデバッグとかできればなんかいろいろできるかもということで探すとちゃんとUSBデバッグはあります。

「アプリ」→「端末設定」→「情報」→「開発者オプション」→「USBデバッグ」で有効になるので有効にして、USBケーブルをつなぐと許可するかどうかのダイアログが表示されます。ちなみにUSBケーブルはType-Aなので注意が必要です。

接続後はいつものようにadbでコマンドを実行できshellにも入れます。そこで早速apkをインストールしてみようとすると次のようなエラーが発生しました。

adb -s 0123456789 install "app-release.apk"
adb: failed to install app-release.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Verify signature failed]

どうも署名チェックのようなものがあり、そこで引っかかっているようです。

提供元不明のアプリのインストールを許可する必要があるのか?と思ったので設定画面を開いてセキュリティから許可してみましたがダメです。Webを検索してもそれらしい情報はないので端末固有でロックをかけていそうです。うーん。

なお、設定画面は am コマンドを使えば開けます。

$ am start -a android.intent.action.MAIN -n com.android.settings/.Settings

ちなみにバージョン情報はこんな感じです。

もう少し探ってみたところ /system/etc/security/apk_sign_white_list.xml というファイルがあり、署名のホワイトリストのようなものだったのでユーザーが任意にインストールするのは無理そうですね(HuaweiやU-NEXTだけが含まれている)。

一応 pm list packageの結果、パッケージの一覧はこんな感じでした。

まとめ

残念ながらAndroid端末としてはいじりがいがないのでそこを期待してはダメそうです。とはいえ特定サービス向けの装置なのでそこはしょうがないかなーという気もします。

U-NEXTのSTBとしてはきびきび動いたりするので普通に使う分には割とよいのではないかとは思いますが、実際お金を出す場合半額の4,980円でもどうかなー…って感じがしますね。リテラシがなくてつなげば使えるみたいなのを求めていない限り、正直縛りのないAndroid TVであるAirStick 4Kの方がよさそう。

そうそう、ログイン必須なので退会すると完全に使い物にならなくなります。

App Centerでユーザー定義環境変数を取得できない

Created at:

Visual Studio App Centerではビルド時の環境変数を定義でき、ビルドスクリプトやGradleのようなビルドシステムから値を取得できるのですが、定義しても環境変数が取れないという現象が発生しました。

tl;dr

SECRET のようなApp Centerが許可しないキーワードを含んだ環境変数を定義している場合、そのままの名前で露出しないようになっているようです。PASSWORD のような単語は通るので SECRET のみの制限かもしれません(ドキュメントに特に書いてない)。

ユーザー定義の環境変数は USER-DEFINED_ というプレフィックスがついた環境変数も定義されるのでそちらから取得する方法もありますが、ハイフンを含んでいるため参照も定義も一筋縄ではいかないのでお勧めできません。

問題と調査

API_KEYNUGET_USERNAME のような環境変数を定義してみたところ特に問題なく取得できたので、もしかして変数名に問題があるのでは?と思いいくつかのパターンを試してみました。すると以下のパターンで違いが出ることがわかりました。

この定義を設定した上でビルドし、pre-buildスクリプトで環境変数をすべて出力すると…

##[section]Starting: Pre Build Script
==============================================================================
Task         : Shell Script
Description  : Run a shell script using bash
Version      : 2.1.3
Author       : Microsoft Corporation
Help         : [More Information](https://go.microsoft.com/fwlink/?LinkID=613738)
==============================================================================
[command]/bin/bash /Users/vsts/agent/2.127.0/work/1/s/app/appcenter-pre-build.sh
...snip...
SECRE__HAUHAU=hauhau3
...snip...
USER-DEFINED_SEECRET_HAUHAU=hauhau2
...snip...
USER-DEFINED_SECRE__HAUHAU=hauhau3
...snip...
USER-DEFINED_SECRET_HAUHAU=hauhau1
...snip...
SEECRET_HAUHAU=hauhau2
...snip...

…となり、SECRET_HAUHAU という変数のみそのまま露出しないようになっています。この結果からApp Centerは SECRET を含む環境変数をそのまま変数として露出しないようにしているようです。セキュリティのためでしょうか。

というわけで特別必要がなければ SECRET という名前を含めないでおくというのがハマらないポイントです。

App Centerでビルドスクリプトが動かない

Created at:

Visual Studio App Centerでビルドする際、特定の名前でシェルスクリプトを置いておくことでビルドの前後でコマンドを実行できます。

2018年2月時点では Build scripts | Microsoft Doc にある通り、以下のスクリプトを認識します(for UWPなら.ps1)。

ここまでは書いてある通りなのですが、実際リポジトリにビルドスクリプトを含めても一向に実行されないという問題が発生しました。設定画面を見ると Build scripts: ✔ Post-clone と表示され認識はされているようでした。

新しいブランチを作ってビルド設定を追加して試すと問題なく動作したので悩んだのですが、実は Build scripts: ✔ Post-clone となった後に Save もしくは Save & Build で設定を保存する必要があるようです。

逆を言うと、ビルドスクリプトを削除した後も保存しなおさないとビルドがエラーになります。

新しいブランチを作った時に動いたのは、すでにビルドスクリプトが含まれていて、ビルド定義を作る際に Build scripts: ✔ Post-clone となった状態だったからということでした。