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

KumoDictionary: クラウドのNoSQLをIDictionary<TKey, TValue>として使う

Created at:

KumoDictionary というライブラリーを公開しました。

このライブラリは何かというと IDictionary<TKey, TValue> を操作すると、裏側ではネットワークの先にあるNoSQL的なもの、例えばAzure Storage TableやAmazon DynamoDBなどへ透過的に記録、読み取りを行うものです。

作った動機

Azure FunctionsやAWS LambdaのようなServerlessアプリ、もしかしたら単なるCLIで小さいツールといったものを作るということはよくありますが、その際にちょっとしたデータを保存したいということも同時によくあります。

例えばFunctionsでWebhookを作ったとして、特定のパラメータに対して最後にアクセスされた時刻を記録する…などなど。

じゃあデータ保存しましょうとなると、ファイルは永続化できる書き込み先はないし、Redisやデータベース的なデータアクセスはサーバーを用意するのもオーバーキル。そうなるとお手軽なのはNoSQL系かとなるのですが、いざSDKを入れて使ってみるとテーブルの設計と呼び出しがめんどくさいものです。

欲しいのは雑に Dictionary<TKey, TValue> が永続化されてくれるような程度でいいのですがーという気持ちからスタートしています。

IDictionary ということはブロッキングだし筋があまりよくないのではという疑問はごもっともですが、クラウド内であればレイテンシーも小さいことやそもそも低負荷のアプリであることをこのライブラリは期待しています。

パフォーマンスを真面目に気にする段階になったときは、Asyncメソッドをご利用いただくか、そもそも普通にSDKでアクセスしてください。

使い方

使い方はとりあえず Azure Storage Table であれば簡単です。

まずポータルやCLIでストレージアカウントを作成しておきます。

次に KumoDictionary.AzureStorageTable NuGetパッケージをインストールします。

そしてプログラムの頭で一度だけデフォルトのバックエンドの設定を行います。

using KumoDictionary;
using KumoDictionary.Provider;

// Set backend provider for Microsoft Azure Storage Table
var tableName = "MyTestTable";
var connectionString = "DefaultEndpointsProtocol=https;AccountName=...";

KumoDictionaryAzureStorageTableProvider.UseAsDefault(connectionString, tableName);

あとは KumoDictionary<TValue> クラスを使うだけです。ちなみにこれは KumoDictionary<string, TValue> の別名です。

// KumoDictionary を作るときにディクショナリーの名前を付けてあげる
var dict = new KumoDictionary<MyClass>("dictionaryName1");

// インデクサで値をセット
dict["key1"] = new MyClass { ValueA = 1234 };
dict["key2"] = new MyClass { ValueA = 5678 };

// インデクサで値を取得
Console.WriteLine(dict["key1"].ValueA); // => 1234

つまり最初のProviderの設定以外は普通のDictionaryっぽく扱えるのです(もちろん一部機能は実装されていなかったりはしますが)。

DynamoDB の場合

DynamoDBの場合にはテーブルの作成があらかじめ必要ですが、逆を言えばそれぐらいです。

裏側

裏側は単純にMessagePackでシリアライズして適当にストアに保存しているだけです。

ただ、値がもしデータストア側がネイティブで対応している型の場合にはそのままそれを使います。例えばAzure Storage TableやDynamoDBはStringを直接扱えるのでそのまま突っ込むことで管理画面や別なツールからもフレンドリーになるのです。

またキーの方も独自のクラスを使えますが、クラスの構造(プロパティやフィールドの名前、型、順番、数)が変更されると壊れるのであまりお勧めはできません。

雰囲気的にはRedis向けのCloudStructuresに近いですし、Redisに値を詰めたい or 詰めるのでよいのであればCloudStructuresを使うのをお勧めします。

まとめ

これでちょっとしたものを作るときにデータや設定の保存の手助けとなれば幸いです。

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以下なので全く面白くなくて残念ですが、ともあれこれで取れるようになったので無いよりは全然よさそうです。