銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

d3.js と ASP.NET MVC を組み合わせて、ELBのアクセス数をHeatMapで表示する

d3.js Advent Calendar 2013 の3日目のエントリです。

インフラ的ないろいろな数値を視覚化してわかりやすく共有したいなあと思って、最近d3.jsを調べています。で、当然弊社でやっているということでサーバーサイドは ASP.NET MVC (5) です。今日は、 【D3.js】 テーブルを使ったヒートマップ(Google Analytics 可視化) | GUNMA GIS GEEK を参考にして、ELBへのアクセス数を時間ごとに視覚化してみたいと思います。

このエントリはASP.NET C# に詳しくない方も見るかと思いますので、簡単に紹介したいと思います。C# を使ってサーバーサイドのアプリを開発するときのフレームワーク(ランタイム含む)がASP.NET になります。最近C# 自体はUnityであったりMonoであったりといろいろなプラットフォームで動きますが、ASP.NET は Windows Server (通常のデスクトップでも可)上のIIS上で動作します。そして、ASP.NET で MVC スタイルで開発するフレームワークが ASP.NET MVC になります。

実際にプロジェクトを作るところを紹介してみます。Visual Studio 2013 Express for Web を使えば無償で開発ができます。また、Nugetと呼ばれるパッケージ管理ツールからd3jsをインストールすることが可能です。Nuget本体はNuGet Package Manager extension からインストールできます。 作られたプロジェクトを右クリックして、Nugetパッケージの管理を選択し、「d3」といれて検索してでてきたものをインストールすることができます。

f:id:tanaka733:20131203125320p:plain

すると、こんな感じにScriptフォルダ以下にjqueryなどを含めて配置されます。

f:id:tanaka733:20131203125404p:plain

さて、コードの説明になりますが、d3.jsを使ったクライアントのコードはほぼほぼ参考にさせていただいたブログにあるコードになります。ASP.NET のサーバーサイドでは、AWS SDKを使ってELBのアクセス数を取得し、データとして整形してJSONとして返す処理を行います。返すデータの形式はこんな感じになります。

{
    "MaxValue": 16893736,
    "MinValue": 36542,
    "Groups": [
        {
            "Key": "2013-11-18T00:00:00+09:00",
            "Values": [
                {
                    "Date": "2013-11-18T00:00:00+09:00",
                    "Hour": 13,
                    "Y": 2219922
                },
                {
                    "Date": "2013-11-18T00:00:00+09:00",
                    "Hour": 14,
                    "Y": 2151392
                },
                {
                    "Date": "2013-11-18T00:00:00+09:00",
                    "Hour": 15,
                    "Y": 2443418
                },
                {
                    "Date": "2013-11-18T00:00:00+09:00",
                    "Hour": 16,
                    "Y": 2387920
                },
                {
                    "Date": "2013-11-18T00:00:00+09:00",
                    "Hour": 17,
                    "Y": 3021549
                },
                {
                    "Date": "2013-11-18T00:00:00+09:00",
                    "Hour": 18,
                    "Y": 4791504
                },
                {
                    "Date": "2013-11-18T00:00:00+09:00",
                    "Hour": 19,
                    "Y": 5694090
                },
                {
                    "Date": "2013-11-18T00:00:00+09:00",
                    "Hour": 20,
                    "Y": 8901460
                },
                {
                    "Date": "2013-11-18T00:00:00+09:00",
                    "Hour": 21,
                    "Y": 13231491
                },
                {
                    "Date": "2013-11-18T00:00:00+09:00",
                    "Hour": 22,
                    "Y": 15825814
                },
                {
                    "Date": "2013-11-18T00:00:00+09:00",
                    "Hour": 23,
                    "Y": 12477543
                }
            ]
        },
//以下繰り返し
    ]
}

データのグラデーションを設定するための最大と最小値もサーバーサイドで計算して返しています。

そして、この処理を行っているサーバーサイドの処理はこんな感じになります。

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Amazon;
using Amazon.CloudWatch;
using Amazon.CloudWatch.Model;

namespace D3js.Samples.Controllers
{
    public class ElbAccessSummaryController : ApiController
    {
        public async Task<SummaryResult> GetDataBlob()
        {
            var cloudwatch = new AmazonCloudWatchClient(RegionEndpoint.APNortheast1);
            var res = await cloudwatch.GetMetricStatisticsAsync(new GetMetricStatisticsRequest()
            {
                Namespace = @"AWS/ELB",
                MetricName = "RequestCount",
                Statistics = new List<string>() { "Sum" },
                Unit = StandardUnit.Count,
                Dimensions = new List<Dimension>()
                {
                    new Dimension() { Name = "LoadBalancerName", Value = ConfigurationManager.AppSettings["ELBName"] },
                    new Dimension() { Name = "AvailabilityZone", Value = ConfigurationManager.AppSettings["AvailabilityZone"] }
                },
                StartTime = DateTime.UtcNow - TimeSpan.FromDays(30),
                EndTime = DateTime.UtcNow,
                Period = 60 * 60
            });
            return new SummaryResult()
            {
                //列挙を繰り返しているので効率はよくない
                MaxValue = (int)res.Datapoints.Max(d => d.Sum),
                MinValue = (int)res.Datapoints.Min(d => d.Sum),
                Groups = res.Datapoints.Select(d => new DateData() { Date = d.Timestamp.Date, Hour = d.Timestamp.Hour, Y = d.Sum })
                    .GroupBy(d => d.Date)
                    .OrderBy(d => d.Key)
                    .ToDictionary(d => d.Key, d => d.OrderBy(da => da.Hour).ToList())
                    .Select(pair => new DateGroup()
                    {
                        Key = pair.Key,
                        Values = pair.Value,
                    })
            };
        }
    }

    public class SummaryResult
    {
        public int MaxValue;
        public int MinValue;
        public IEnumerable<DateGroup> Groups;
    }

    public class DateGroup
    {
        public DateTime Key { get; set; }
        public IEnumerable<DateData> Values { get; set; }
    }

    public class DateData
    {
        public DateTime Date { get; set; }
        public int Hour { get; set; }
        public double Y { get; set; }
    }
}

ASP.NET MVC ではRESTスタイルでのアクセスになっており、/api/ElbAccessSummary へのGETリクエストをこのメソッドで受けます(デフォルトの命名規約によりルーティングされます。)。最初にAWS SDKを使って、ELBの時間ごとのアクセス数を取得しています。続いて、取得したデータをLINQというコレクション操作用のライブラリを使って処理しています。1日ごとにグルーピングし、並び替えて、集計処理を行って、JSONに対応したデータクラスを生成しています。

続いて表示するページです。Index.cshtmlというASP.NET MVC の Razorと呼ばれるテンプレートエンジンで記述したコードですが、ほぼほぼ単なる静的HTMLです。

@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>@ViewBag.Title</title>
    <script src="@Url.Content("~/Scripts/jquery-1.10.2.min.js")"></script>
    <script src="@Url.Content("~/Scripts/modernizr-2.6.2.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/d3.v3.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/d3.helper.js")" type="text/javascript"></script>
    <style type="text/css">
    </style>
</head>
<body>
    <svg></svg>
    <script type="text/javascript">
        d3.json('/api/ElbAccessSummary', function (data) {
            var weekday = ["日", "月", "火", "水", "木", "金", "土"];

            var max = data.MaxValue;
            var min = data.MinValue;
            var svg = d3.select('svg').data([data.Groups]);
            var colorScale = d3.scale.linear().domain([0, min, (min + max) / 2, max]).range(["#FFFFFF", "#FFF2F2", "#FF7F7F", "#FF0000"]);
            var parseDate = d3.time.format.iso.parse;
            var groupAttr = {
                transform: function () {
                    var m = 10;
                    return function (d, i) {
                        if (parseDate(d.Key).getDay() == 0) m += 10; 
                        return "translate(" + [200, m + (i * 16)] + ")";
                    };
                }(),
                width: 10,
                height: 10
            };

            var group = svg.selectAll('g')
                .data(D())
                .enter()
                .append('g')
                .attr(groupAttr);

            var ylabelAttr = {
                x: -100,
                y: 10,
                "text-anchor": "middle",
                "aligbment-baseline": "center",
                fill: "black",
                stroke: "none"
            };

            var ylabel = group.append('text')
                .attr(ylabelAttr)
                .text(function (d) { return parseDate(d.Key).toLocaleDateString() + ":" + weekday[parseDate(d.Key).getDay()]; });

            var rectAttr = {
                x: F('Hour', '* 20'),
                y: 0,
                width: 10,
                height: 10,
                fill: F('Y', colorScale)
            };

            var rect = group.selectAll('rect')
                .data(F('Values'))
                .enter()
                .append('rect')
                .attr(rectAttr);

            rect.append('title').text(function (d) { return parseDate(d.Date).toLocaleDateString() + " " + d.Hour + "時 アクセス:" + d.Y; }); //tooltip追加
        }).header("Content-Type", "application/json");
    </script>
</body>
</html>

JSONとして返すデータの構造が違う以外は参考にしたブログそのままです... 【D3.js】作っておくと便利な関数達 | GUNMA GIS GEEK で紹介されていたヘルパー関数もプロジェクトに追加しています。

で、実行するとこんな感じにとある期間におけるアクセス数が表示されます。 (意図的にグラデーションわかりづらくしています。サンプルコードのまま実行すればそれなりに見やすくなります)

f:id:tanaka733:20131203131147p:plain

というわけで、ASP.NETの紹介なのかd3.jsなのかわからない感じですが、お手軽に視覚化できるよ!というお話でした。引き続きd3.js には注目していきます!

また、今回のサンプルコードはgithubで公開しています。動かすときは、User.Sample.configを開いて、AWS SDK(API)の認証キーとデータを取得するELBの名前とAZを指定した上で、このファイルをUser.configにリネームしてください。

tanaka-takayoshi/D3js.ASPNet.Samples · GitHub