Webサーバーのログ情報を視覚化する
サーバーエンジニアをやってると、シェルスクリプトでログ情報を編集してホームページなどのアクセスランクを作ることは多いと思いますが、経営者やデザイナーなどエンジニア以外の人にとっては、その(ほとんど生な)データは見ても情報を見抜きにくい上、面白くないのでアイデアがわきにくいと思います。
そこで、このつまらないサーバーログをProcessingを使ってわかりやすくネットワークグラフとして表示するプログラムを紹介します。
グラフの描画に関しては「ビジュアライジング・データ」を参考に作成しました。
ビジュアライジング・データ ―Processingによる情報視覚化手法
- 作者: Ben Fry,増井俊之(監訳),加藤慶彦
- 出版社/メーカー: オライリージャパン
- 発売日: 2008/12/01
- メディア: 大型本
- 購入: 35人 クリック: 718回
- この商品を含むブログ (65件) を見る
まずは、サーバーのログをProcessingで扱いやすいように整形します。
#!/bin/bash log=/var/log/nginx/access.log #path to log file # except the acccess history of me # then, shape it, and save as a text cat $log | grep -v 192.168.10.3 | cut -d ' ' -f 6 | sort | uniq -c | sort -r | grep .html | sed -r 's/.html//g' > access_rank.txt
実行結果は以下のようになります。
... 2 company message 2 company index 2 company companyinfo 1 update character 1 sitemap 1 site introduction index 1 service 1 ricruit ...
今回は最近作成したNginxのサーバーのログファイルを整形したので、情報量が少なかったのです。
さて、この空白で区切られたデータを表示するプログラムを紹介していきます。
ネットワークを構成するためには「ノード」と「エッジ」が必要になります。今回は「ノード」がHTMLページ、「エッジ」が「ノード」つなぐための「線」になります。
まずは、この2つをクラスによって定義していきます。
class Node { float x, y; float dx, dy; boolean fixed; String label; int count; Node(String label) { this.label = label; x = random(width); y = random(height); } void relax() { float ddx = 0; float ddy = 0; for (int j = 0; j < nodeCount; j++) { Node n = nodes[j]; if (n != this) { float vx = x - n.x; float vy = y - n.y; float lensq = vx * vx + vy * vy; if (lensq == 0) { ddx += random(1); ddy += random(1); } else if (lensq < 100*100) { ddx += vx / lensq; ddy += vy / lensq; } } } float dlen = mag(ddx, ddy) / 2; if (dlen > 0) { dx += ddx / dlen; dy += ddy / dlen; } } void update() { if (!fixed) { x += constrain(dx, -5, 5); y += constrain(dy, -5, 5); x = constrain(x, 0, width); y = constrain(y, 0, height); } dx /= 2; dy /= 2; } void draw() { if (fixed) { fill(nodeColor); stroke(0); strokeWeight(0.5); String content = label + " " + count; float w = textWidth(content) + 10; float h = textAscent() + textDescent() + 4; ellipse(x, y, w*pow(1.06, count-1), h*pow(1.06, count+1)); fill(0); textAlign(CENTER, CENTER); text(content, x, y); } else { fill(nodeColor); stroke(0); strokeWeight(0.5); ellipse(x, y, sqrt(count)*10, sqrt(count)*10); } } void increment() { count++; } } Node findNode(String label) { label = label.toLowerCase(); Node n = (Node) nodeTable.get(label); if (n == null) { return addNode(label); } return n; } Node addNode(String label) { Node n = new Node(label); if (nodeCount == nodes.length) { nodes = (Node[]) expand(nodes); } nodeTable.put(label, n); nodes[nodeCount++] = n; return n; } class Edge { Node from; Node to; float len; int count; Edge(Node from, Node to) { this.from = from; this.to = to; this.len = 50; } void relax() { float vx = to.x - from.x; float vy = to.y - from.y; float d = mag(vx, vy); if (d > 0) { float f = (len -d) / (d * 3); float dx = f * vx; float dy = f * vy; to.dx += dx; to.dy += dy; from.dx -= dx; from.dy -= dy; } } void draw() { stroke(edgeColor); strokeWeight(0.35); line(from.x, from.y, to.x, to.y); } void increment() { count++; } } void addEdge(String fromLabel, String toLabel) { Node from = findNode(fromLabel); Node to = findNode(toLabel); from.increment(); to.increment(); // check whether the Edge have already existed. for (int i = 0; i < edgeCount; i++) { if (edges[i].from == from && edges[i].to == to) { edges[i].increment(); return; } } Edge e = new Edge(from, to); e.increment(); if (edgeCount == edges.length) { edges = (Edge[]) expand(edges); } edges[edgeCount++] = e; }
クラス内のメゾッドにrelax(), update(), draw()があり、これらがグラフを上手く表示するようにエッジやノードの位置を調節するそうです。詳しいことは他の文献を参考にしろとのことでした。
また、ビジュアライジング・データには載っていませんでしたが、アクセス数の多いページのノードを大きく表示するように自身で改良しました。
ゲーム開発のための物理シミュレーション入門―Physics for Game Developers
- 作者: David M.Bourg,榊原一矢
- 出版社/メーカー: オーム社
- 発売日: 2003/07
- メディア: 単行本
- 購入: 2人 クリック: 122回
- この商品を含むブログ (15件) を見る
さて、続いてエッジに先ほどのデータを追加していきます。
void loadData() { reader = createReader("/path/to/access_rank.txt"); // 先ほどシェルスクリプトで整形したデータへのパス try { String line = reader.readLine(); while (line != null) { String[] columns = split(line, ' '); String fromEdge = INDEX; int num = 0; for (String word : columns) { int count = 0; if (!word.isEmpty() && isNumeric(word)) { num = Integer.parseInt(word); } else if (!word.isEmpty() && !word.toLowerCase().contains(INDEX) && num != 0) { for (int i = 0; i < num; i++) addEdge(fromEdge, word); fromEdge = word; } } line = reader.readLine(); } } catch (IOException e) { e.printStackTrace(); } } public static boolean isNumeric(String str) { try { Integer d = Integer.parseInt(str); } catch(NumberFormatException nfe) { return false; } return true; }
これはProcessingのsetup()関数で呼び出されます。一行ごとにページヘのパスを調べてネットワークを構成します。
プログラムの大まかな構成は上記のような感じです。あとはマウスがクリックされた時の振る舞いや、ノードの色などを決めたりするだけなので、その辺りは読み飛ばしていってもらったらいいかと思います。
最後にプログラムの全体を掲載しておきます。
BufferedReader reader; int nodeCount; Node[] nodes = new Node[100]; HashMap nodeTable = new HashMap(); Node selection; int edgeCount; Edge[] edges = new Edge[500]; static final color nodeColor = #F0C070; static final color selectColor = #FF3030; static final color fixedColor = #FF8080; static final color edgeColor = #000000; PFont font; static final String INDEX = "index"; void setup() { size(1000, 600); loadData(); font = createFont("SansSerif", 10); textFont(font); smooth(); } void loadData() { reader = createReader("/path/to/access_rank.txt"); try { String line = reader.readLine(); while (line != null) { String[] columns = split(line, ' '); String fromEdge = INDEX; int num = 0; for (String word : columns) { int count = 0; if (!word.isEmpty() && isNumeric(word)) { num = Integer.parseInt(word); } else if (!word.isEmpty() && !word.toLowerCase().contains(INDEX) && num != 0) { for (int i = 0; i < num; i++) addEdge(fromEdge, word); fromEdge = word; } } line = reader.readLine(); } } catch (IOException e) { e.printStackTrace(); } } public static boolean isNumeric(String str) { try { Integer d = Integer.parseInt(str); } catch(NumberFormatException nfe) { return false; } return true; } void draw() { background(255); for (int i = 0; i < edgeCount; i++) edges[i].relax(); for (int i = 0; i < nodeCount; i++) nodes[i].relax(); for (int i = 0; i < nodeCount; i++) nodes[i].update(); for (int i = 0; i < edgeCount; i++) edges[i].draw(); for (int i = 0; i < nodeCount; i++) nodes[i].draw(); } void mousePressed() { float closest = 20; for (int i = 0; i < nodeCount; i++) { Node n = nodes[i]; float d = dist(mouseX, mouseY, n.x, n.y); if (d < closest) { selection = n; closest = d; } } if (selection != null) { if (mouseButton == LEFT) { selection.fixed = true; } else if (mouseButton == RIGHT) { selection.fixed = false; } } } void mouseDragged() { if (selection != null) { selection.x = mouseX; selection.y = mouseY; } } void mouseRelesed() { selection = null; } class Node { float x, y; float dx, dy; boolean fixed; String label; int count; Node(String label) { this.label = label; x = random(width); y = random(height); } void relax() { float ddx = 0; float ddy = 0; for (int j = 0; j < nodeCount; j++) { Node n = nodes[j]; if (n != this) { float vx = x - n.x; float vy = y - n.y; float lensq = vx * vx + vy * vy; if (lensq == 0) { ddx += random(1); ddy += random(1); } else if (lensq < 100*100) { ddx += vx / lensq; ddy += vy / lensq; } } } float dlen = mag(ddx, ddy) / 2; if (dlen > 0) { dx += ddx / dlen; dy += ddy / dlen; } } void update() { if (!fixed) { x += constrain(dx, -5, 5); y += constrain(dy, -5, 5); x = constrain(x, 0, width); y = constrain(y, 0, height); } dx /= 2; dy /= 2; } void draw() { if (fixed) { fill(nodeColor); stroke(0); strokeWeight(0.5); String content = label + " " + count; float w = textWidth(content) + 10; float h = textAscent() + textDescent() + 4; ellipse(x, y, w*pow(1.06, count-1), h*pow(1.06, count+1)); fill(0); textAlign(CENTER, CENTER); text(content, x, y); } else { fill(nodeColor); stroke(0); strokeWeight(0.5); ellipse(x, y, sqrt(count)*10, sqrt(count)*10); } } void increment() { count++; } } Node findNode(String label) { label = label.toLowerCase(); Node n = (Node) nodeTable.get(label); if (n == null) { return addNode(label); } return n; } Node addNode(String label) { Node n = new Node(label); if (nodeCount == nodes.length) { nodes = (Node[]) expand(nodes); } nodeTable.put(label, n); nodes[nodeCount++] = n; return n; } class Edge { Node from; Node to; float len; int count; Edge(Node from, Node to) { this.from = from; this.to = to; this.len = 50; } void relax() { float vx = to.x - from.x; float vy = to.y - from.y; float d = mag(vx, vy); if (d > 0) { float f = (len -d) / (d * 3); float dx = f * vx; float dy = f * vy; to.dx += dx; to.dy += dy; from.dx -= dx; from.dy -= dy; } } void draw() { stroke(edgeColor); strokeWeight(0.35); line(from.x, from.y, to.x, to.y); } void increment() { count++; } } void addEdge(String fromLabel, String toLabel) { Node from = findNode(fromLabel); Node to = findNode(toLabel); from.increment(); to.increment(); // check whether the Edge have already existed. for (int i = 0; i < edgeCount; i++) { if (edges[i].from == from && edges[i].to == to) { edges[i].increment(); return; } } Edge e = new Edge(from, to); e.increment(); if (edgeCount == edges.length) { edges = (Edge[]) expand(edges); } edges[edgeCount++] = e; }
そして、実行した結果がこちらです。
やっぱりデータが少ないので、少しさびしい印象を与えますね^^;
今回はホームページのアクセスログを解析しました。なのでノードの数は少なくて観やすかったと思います。(ページの数が50を超えるような大規模なホームページの場合はかなり見難いと思いますが)
このように、ノード数が少ないデータを可視化したいのなら、Processingでネットワークグラフを作成してみるといいかと思います。