Skip to content
Browse files

Merge pull request #535 from mmzeeman/z_stats

Z stats
  • Loading branch information...
2 parents dc1adf0 + fe450d6 commit 19ebc178c75a6836e606c424ec987f684140e900 @mmzeeman mmzeeman committed Mar 14, 2013
View
1 GNUmakefile
@@ -53,6 +53,7 @@ compile-zotonic: $(PARSER).erl erl ebin/$(APP).app
compile: compile-deps compile-zotonic
+
# Generate documentation
.PHONY: docs edocs
docs:
1 deps/bear
@@ -0,0 +1 @@
+Subproject commit 0da736b0e9bef2c7150cd6e6c4a9fa1854deedf9
1 deps/folsom
@@ -0,0 +1 @@
+Subproject commit 23c96ef175bbc796a59bdfbac7ceb816579f3fab
1 deps/meck
@@ -0,0 +1 @@
+Subproject commit 27ed56adcf39616f041d0057c8feb32067ffcb2f
View
22 doc/ref/models/model_stats.rst
@@ -0,0 +1,22 @@
+
+.. include:: meta-stats.rst
+
+Access the statistics from templates using the `stats` model.
+
+The `metrics` property holds a list of all available statistics.
+
+To get the information about a metric there are keys for `system`,
+`name`, `type` and `value`. Example, listing all raw data::
+
+ {% for metric in m.stats.metrics %}
+ {{ metric.system }}.{{ metric.name }} ({{ metric.type }}): {{ metric.value|pprint }}<br />
+ {% endfor %}
+
+To get at a specific statistic, you need to provide system and name for it.
+
+This example will retrieve the statistics for the db requests metric::
+
+ {{ m.stats.db.requests.value }}
+
+
+.. note:: This model is experimental and may change without notice.
View
4 include/zotonic.hrl
@@ -18,11 +18,9 @@
%% The release information
-include("zotonic_release.hrl").
-
-include("zotonic_notifications.hrl").
-
-include("zotonic_events.hrl").
-
+-include("zotonic_stats.hrl").
-include_lib("webzmachine/include/wm_reqdata.hrl").
%% @doc The request context, session information and other
View
11 include/zotonic_stats.hrl
@@ -0,0 +1,11 @@
+%% Collection of statistic operations.
+
+-record(stats_from,
+ {host=zotonic,
+ system=core
+ }).
+
+-record(counter, {name, op=incr, value=1}).
+
+-record(histogram, {name, value=undefined}).
+
View
4 modules/mod_admin_stats/dispatch/dispatch
@@ -0,0 +1,4 @@
+%% -*- mode: erlang -*-
+[
+ {admin_stats, ["admin", "stats"], controller_template, [{template, "admin_stats_overview.tpl"}, {acl, {use, mod_admin_stats}}, {ssl, true}]}
+].
View
64 modules/mod_admin_stats/lib/css/charts.css
@@ -0,0 +1,64 @@
+svg {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.bar rect {
+// fill: rgba(2, 176, 232, 0.75);
+ fill: rgb(6, 156, 209);
+ fill-opacity: 0.75;
+ shape-rendering: crispEdges;
+}
+
+.bar rect:hover {
+ fill-opacity: 1;
+}
+
+.bar text {
+ fill: #fff;
+}
+
+.axis path, .axis line {
+ fill: none;
+ stroke: #000;
+ stroke-opacity: .25;
+ shape-rendering: crispEdges;
+}
+
+path.line {
+ fill: none;
+ stroke-width: 2;
+ stroke-opacity: 0.5;
+}
+
+path.line:hover {
+ stroke-opacity: 1;
+}
+
+.serie-0 {
+ stroke: rgb(6, 156, 209);
+}
+
+.serie-1 {
+ stroke: #ff7f0e;
+}
+
+.serie-2 {
+ stroke: #2ca02c;
+}
+
+.serie-3 {
+ stroke: #d62728;
+}
+
+.serie-4 {
+ stroke: #9467bd;
+}
+
+.chart-label:after {
+ content: ": ";
+}
View
26 modules/mod_admin_stats/lib/d3.LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2012, Michael Bostock
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* The name Michael Bostock may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
View
198 modules/mod_admin_stats/lib/js/charts/histogram-duration.js
@@ -0,0 +1,198 @@
+/* This is a adapted version of the duration histogram
+ example by Mike Bostock: http://bl.ocks.org/mbostock/3048166 */
+
+function histogram_duration_chart() {
+ // declare private vars
+ var formatTime = d3.time.format("%H:%M");
+ var client_width, client_height;
+
+ // declare public properties
+ var props = {
+ // Formatters for counts and times (converting numbers to Dates).
+ format: {
+ x: function(d) {
+ return formatTime(new Date(2012, 0, 1, 0, d)); },
+ y: d3.format(",.0f")
+ },
+
+ // chart margins and size
+ margin: {top: 10, right: 30, bottom: 30, left: 50},
+ width: 600,
+ height: 200,
+
+ // scales, axis and data
+ x: d3.scale.linear(),
+ y: d3.scale.linear(),
+ axis: {
+ x: d3.svg.axis(),
+ y: d3.svg.axis()
+ },
+
+ // callback functions for updating axis ticks
+ ticks: {
+ x: null,
+ y: null
+ },
+
+ // e.g. transition duration, in ms
+ animation_speed: 500,
+ };
+
+ // this function is responsible for visualizing the chart
+ function chart(selection) {
+ // define chart redraw function
+ chart.update = function(data) {
+ var svg = d3.select(this).select("svg g");
+
+ // update axis domains
+ props.x.domain([0, d3.max(data, function(d){ return d.x })]);
+ props.y.domain([0, d3.max(data, function(d){ return d.y })]);
+
+ // update axis ticks
+ if (props.ticks.x)
+ props.ticks.x.apply(chart, [props.axis.x, data]);
+ if (props.ticks.y)
+ props.ticks.y.apply(chart, [props.axis.y, data]);
+
+ function top(d) {
+ return props.y(d.y);
+ }
+
+ function left(d, i) {
+ return i > 0 ? props.x(data[i-1].x) : 0;
+ }
+
+ function width(d, i) {
+ return props.x(d.x) - left(d, i) - 1;
+ }
+
+ // draw bars!
+ var bar = svg.selectAll(".bar")
+ .data(data);
+
+ // update existing
+ bar.transition()
+ .attr("transform", function(d, i) {
+ return "translate("
+ + left(d, i) + ","
+ + top(d) + ")"; })
+ .select("rect")
+ .attr("width", function(d, i) { return width(d, i) })
+ .attr("height", function(d) {
+ return client_height - top(d); });
+
+ // enter added bars
+ bar.enter().append("g")
+ .attr("class", "bar")
+ .attr("transform", function(d, i) {
+ return "translate("
+ + left(d, i) + ","
+ + top(d) + ")"; })
+ .append("rect")
+ .attr("x", 1)
+ .attr("height", function(d) {
+ return client_height - top(d); })
+ .style("fill-opacity", 1e-6)
+ .transition()
+ .duration(props.animation_speed)
+ .attr("width", function(d, i) { return width(d, i) })
+ .style("fill-opacity", 1)
+ .each("end", function(){
+ d3.select(this).style("fill-opacity", null) });
+
+ // exit dropped bars
+ bar.exit().transition()
+ .attr("height", 0)
+ .style("fill-opacity", 1e-6)
+ .remove();
+
+ // redraw axis
+ svg.select(".x.axis").transition()
+ .duration(props.animation_speed)
+ .call(props.axis.x);
+ svg.select(".y.axis").transition()
+ .duration(props.animation_speed)
+ .call(props.axis.y);
+
+ return chart;
+ };
+
+ // create chart svg object for each selected object
+ selection.each(function (data) {
+ // store reference to this chart in the node
+ this.chart = chart;
+
+ client_width = props.width
+ - props.margin.left
+ - props.margin.right;
+ client_height = props.height
+ - props.margin.top
+ - props.margin.bottom;
+
+ // produce sample graph if no data provided
+ if (!data)
+ {
+ // Generate a log-normal distribution with a median of 30 minutes.
+ var values = d3.range(1000)
+ .map(d3.random.logNormal(Math.log(30), .4));
+
+ // dummy domain, needed to get proper ticks for the bins
+ props.x.domain([0, 120]);
+
+ // Generate a histogram using twenty uniformly-spaced bins.
+ data = d3.layout.histogram()
+ .bins(props.x.ticks(20))(values);
+ }
+
+ // setup input domain and output range
+ props.x
+ .domain([0, d3.max(data, function(d){ return d.x })])
+ .range([0, client_width]);
+ props.y
+ .domain([0, d3.max(data, function(d){ return d.y })])
+ .range([client_height, 0]);
+
+ // setup axis
+ props.axis.x
+ .scale(props.x)
+ .orient("bottom")
+ .tickFormat(props.format.x);
+ props.axis.y
+ .scale(props.y)
+ .orient("left")
+ .tickFormat(props.format.y);
+
+ // create svg
+ var svg = d3.select(this).append("svg")
+ .attr("width", props.width)
+ .attr("height", props.height)
+ .append("g")
+ .attr("transform", "translate("
+ + props.margin.left + ","
+ + props.margin.top + ")");
+
+ // add axis
+ svg.append("g")
+ .attr("class", "x axis")
+ .attr("transform", "translate(0," + client_height + ")");
+
+ svg.append("g")
+ .attr("class", "y axis");
+
+ // draw initial chart, and we're done
+ return chart.update.apply(this, [data]);
+ });
+
+ return chart;
+ }
+
+ chart.call = function(callback) {
+ callback.apply(this, Array.prototype.slice.call(arguments, 1));
+ return this;
+ };
+
+ z_charts.proxy_properties(props, chart);
+
+ // return our properly propertized chart function object
+ return chart;
+}
View
157 modules/mod_admin_stats/lib/js/charts/line-chart.js
@@ -0,0 +1,157 @@
+function line_chart() {
+ var props = {
+ // Formatters for counts and times (converting numbers to Dates).
+ format: {
+ x: function(d) {
+ return formatTime(new Date(2012, 0, 1, 0, d)); },
+ y: d3.format(",.0f")
+ },
+
+ // chart margins and size
+ margin: {top: 10, right: 30, bottom: 30, left: 50},
+ width: 600,
+ height: 200,
+
+ // scales, axis and data
+ x: d3.time.scale(),
+ y: d3.scale.linear(),
+ axis: {
+ x: d3.svg.axis(),
+ y: d3.svg.axis()
+ },
+
+ // callback functions for updating axis ticks
+ ticks: {
+ x: null,
+ y: null
+ },
+
+ line: d3.svg.line()
+ .x(function(d){ return props.x(d.x); })
+ .y(function(d){ return props.y(d.y); }),
+
+ // e.g. transition duration, in ms
+ animation_speed: 500,
+ };
+
+ function chart(selection) {
+ chart.update = function(data) {
+ var svg = d3.select(this).select("svg g");
+
+ // input domain and output range
+ // for now, we only look at all series for y input domain
+ // for all else, we only look at the first series.
+ props.x
+ .domain([Date.now() - 300 * 1000, Date.now()])
+ //d3.min(data[0], function(d){ return d.x }),
+ //d3.max(data[0], function(d){ return d.x })])
+ .range([0, client_width]);
+ props.y
+ .domain([0, d3.max(data, function(s) {
+ return d3.max(s, function(d){
+ return d.y })})])
+ .range([client_height, 0]);
+
+ // update axis ticks
+ if (props.ticks.x)
+ props.ticks.x.apply(chart, [props.axis.x, data[0]]);
+ if (props.ticks.y)
+ props.ticks.y.apply(chart, [props.axis.y, data[0]]);
+
+ // redraw line!
+ svg.selectAll("path.line")
+ .transition()
+ .duration(props.animation_speed)
+ .attr("d", props.line);
+
+ // redraw axis
+ svg.select(".x.axis").transition()
+ .duration(props.animation_speed)
+ .call(props.axis.x);
+ svg.select(".y.axis").transition()
+ .duration(props.animation_speed)
+ .call(props.axis.y);
+
+ return chart;
+ }
+
+ // create chart svg object for each selected object
+ selection.each(function (data) {
+ // store reference to this chart in the node
+ this.chart = chart;
+
+ client_width = props.width
+ - props.margin.left
+ - props.margin.right;
+ client_height = props.height
+ - props.margin.top
+ - props.margin.bottom;
+
+ // produce sample graph if no data provided
+ if (!data)
+ {
+ data = [];
+ for (var i = 0; i < 5; i++) {
+ var random = d3.random.normal(5+5*i);
+ data.push(d3.range(50).map(function(j){
+ return { x: Date.now() + 10000 * j, y: random() } }));
+ }
+ }
+
+ // setup axis
+ props.axis.x
+ .scale(props.x)
+ .orient("bottom");
+ //.tickFormat(props.format.x);
+ props.axis.y
+ .scale(props.y)
+ .orient("left");
+ //.tickFormat(props.format.y);
+
+ // create svg
+ var svg = d3.select(this).append("svg")
+ .attr("width", props.width)
+ .attr("height", props.height)
+ .append("g")
+ .attr("transform", "translate("
+ + props.margin.left + ","
+ + props.margin.top + ")");
+
+ svg.append("defs").append("svg:clipPath")
+ .attr("id", "clip")
+ .append("rect")
+ .attr("width", client_width)
+ .attr("height", client_height);
+
+ // add axis
+ svg.append("g")
+ .attr("class", "x axis")
+ .attr("transform", "translate(0," + client_height + ")");
+
+ svg.append("g")
+ .attr("class", "y axis");
+
+ // add line series
+ svg.append("g")
+ .attr("class", "series")
+ .attr("clip-path", "url(#clip)")
+ .selectAll("path").data(data)
+ .enter()
+ .append("path")
+ .attr("class", function(d, i) { return "line serie-" + i });
+
+
+ // draw chart & we're done!
+ return chart.update.apply(this, [data]);
+ });
+ }
+
+ chart.call = function(callback) {
+ callback.apply(this, Array.prototype.slice.call(arguments, 1));
+ return this;
+ };
+
+ z_charts.proxy_properties(props, chart);
+
+ return chart;
+}
View
144 modules/mod_admin_stats/lib/js/charts/stats_charts.js
@@ -0,0 +1,144 @@
+function stats_chart_factory() {
+
+ // Chart factory helpers
+
+ // creates a duration histogram chart
+ function histogram() {
+ function update_ticks(axis, input) {
+ var ticks = [], i, p, x, r;
+ r = (input[input.length - 1].x - input[0].x) / 10;
+ p = -r;
+ for( i = 0; i < input.length; i++)
+ {
+ x = input[i].x;
+ if (x - p < r) continue;
+ ticks.push(x);
+ p = x;
+ }
+
+ axis.tickValues(ticks);
+ }
+
+ this.call(
+ histogram_duration_chart().call(function() {
+ this.format().x = d3.format(",.1f");
+ this.ticks().x = update_ticks;
+ }));
+ }
+
+ // creates a line chart
+ function line() {
+ this.call(line_chart());
+ }
+
+ // creates a dynamic "label value" text element
+ function text() {
+ this.append("span")
+ .attr("class", "chart-text")
+ .call(function() {
+ this.append("span")
+ .attr("class", "chart-label")
+ .text(function(d){ return d.label });
+ this.append("span")
+ .attr("class", "chart-value")
+ .text(function(d){ return d.value });
+ });
+ this.node().chart = { update: function(d) {
+ d3.select(this).select(".chart-value")
+ .text(d.value);
+ }};
+ }
+
+ // Chart data helpers
+
+ // create data for the histogram chart
+ function histogram_data(d) {
+ return [
+ { factory: text, datum:
+ { label: "Min", value: d.min }
+ },
+ { factory: text, datum:
+ { label: "Max", value: d.max }
+ },
+ { factory: text, datum:
+ { label: "Mean (geometric)", value: d.mean.geometric }
+ },
+ { factory: text, datum:
+ { label: "Sample count", value: d.count }
+ },
+ { factory: histogram, datum: d.histogram }
+ ];
+ }
+
+ // create data for the line chart
+ function meter_data(d) {
+ var series = ["one", "five", "fifteen", "day"];
+ var now = Date.now();
+ var data = this.length > 0 && d3.select(this[1]).datum();
+ if (!data)
+ data = series.map(function(){
+ return [{x: now - 1000, y: 0}] });
+
+ data.forEach(
+ function(s, i) {
+ s.push({x: now, y: d[this[i]]})
+ },
+ series);
+
+ return [
+ { factory: text, datum:
+ { label: "Total count", value: d.count }
+ },
+ { factory: line, datum: data }
+ ];
+ }
+
+ // chart data middle man
+ function chart_data(d) {
+ var type_data = {
+ histogram: histogram_data,
+ meter: meter_data
+ };
+
+ var data = type_data[d.type].apply(this, [d]);
+
+ if (this.length > 0)
+ for (var i = 0; i < data.length; i++)
+ data[i] = data[i].datum;
+
+ return data;
+ }
+
+ // The factory function creating all charts and stuff
+ function factory(selection) {
+ selection
+ .append("h3").text(function(d){
+ return d.system + " " + d.name });
+
+ selection.selectAll("div")
+ .data(chart_data)
+ .enter()
+ .append("div")
+ .attr("class", "chart-data")
+ .each(function(d){
+ d3.select(this)
+ .datum(d.datum)
+ .call(d.factory);
+ });
+
+ return selection;
+ }
+
+ // The update function propagating new data to all charts and stuff
+ factory.update = function(selection) {
+ selection.selectAll(".chart-data")
+ .data(chart_data)
+ .each(function(d) {
+ if (this.chart && this.chart.update)
+ this.chart.update.apply(this, [d]);
+ });
+ }
+
+ // return the completed factory
+ return factory;
+}
View
43 modules/mod_admin_stats/lib/js/charts/z_charts.js
@@ -0,0 +1,43 @@
+var z_charts = {};
+
+(function($){
+
+ // define property setter/getter function
+ $.define_property = function(obj, name, ret) {
+ return function() {
+ if (!arguments.length)
+ return obj[name];
+ obj[name] = arguments[0];
+ return ret;
+ };
+ };
+
+ // assign properties to chart
+ $.proxy_properties = function (src, dst) {
+ var name;
+ for (name in src) {
+ if (src.hasOwnProperty(name))
+ dst[name] = this.define_property(src, name, dst);
+ }
+ };
+
+ // update all charts in the selection with new data
+ $.update = function(selection, data, chart_factory) {
+ // select charts and assign data
+ var chart = selection.selectAll("div.chart").data(data);
+
+ // update existing
+ chart.call(chart_factory.update);
+
+ // enter added charts
+ chart.enter().append("div")
+ .attr("class", "chart")
+ .call(chart_factory);
+
+ // exit dropped charts
+ chart.exit().transition()
+ .attr("style", "opacity=0")
+ .remove();
+ };
+
+})(z_charts);
View
7,790 modules/mod_admin_stats/lib/js/d3.js
7,790 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
View
4 modules/mod_admin_stats/lib/js/d3.min.js
4 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
View
53 modules/mod_admin_stats/mod_admin_stats.erl
@@ -0,0 +1,53 @@
+%% @author Andreas Stenius <git@astekk.se>
+%% @copyright 2013 Andreas Stenius
+%% Date: 2013-02-23
+%% @doc Statistics for the admin interface.
+
+%% Copyright 2013 Andreas Stenius
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+
+-module(mod_admin_stats).
+-author("Andreas Stenius <git@astekk.se>").
+
+-mod_title("Admin Statistics").
+-mod_description("Provides statistic information.").
+-mod_prio(500).
+
+%% interface functions
+-export([
+ observe_admin_menu/3,
+ observe_postback_notify/2
+ ]).
+
+-include_lib("zotonic.hrl").
+-include_lib("modules/mod_admin/include/admin_menu.hrl").
+
+observe_admin_menu(admin_menu, Acc, Context) ->
+ [#menu_item{ id=admin_stats,
+ parent=admin_system,
+ label=?__("Statistics", Context),
+ url={admin_stats},
+ visiblecheck={acl, use, mod_admin_stats}
+ }
+ |Acc].
+
+observe_postback_notify(#postback_notify{ message="update_metrics" }, Context) ->
+ {Output, Context1} = z_template:render_to_iolist("_metrics_jsdata.tpl", [], Context),
+ z_context:add_script_page(
+ io_lib:format("z_event('new_metrics', ~s);", [z_convert:to_flatlist(Output)]),
+ Context1),
+ Context1;
+observe_postback_notify(Args, _Context) ->
+ ?DEBUG(Args),
+ undefined.
View
30 modules/mod_admin_stats/templates/_metrics_jsdata.tpl
@@ -0,0 +1,30 @@
+[
+{% for metric in m.stats.metrics %}
+ {
+ type: "{{ metric.type }}",
+ system: "{{ metric.system }}",
+ name: "{{ metric.name }}",
+ {% if metric.type == `histogram` %}
+ histogram: [
+ {% for bin, count in metric.value.histogram %}
+ {x: {{ bin/1000 }}, y: {{ count }} },
+ {% endfor %}
+ ],
+ count: {{ metric.value.n }},
+ min: {{ metric.value.min/1000 }},
+ max: {{ metric.value.max/1000 }},
+ mean: {
+ arithmetic: {{ metric.value.arithmetic_mean/1000 }},
+ geometric: {{ metric.value.geometric_mean/1000 }},
+ harmonic: {{ metric.value.harmonic_mean/1000 }}
+ }
+ {% else %}
+ count: {{ metric.value.count }},
+ one: {{ metric.value.one }},
+ five: {{ metric.value.five }},
+ fifteen: {{ metric.value.fifteen }},
+ day: {{ metric.value.day }}
+ {% endif %}
+ },
+{% endfor %}
+]
View
52 modules/mod_admin_stats/templates/admin_stats_overview.tpl
@@ -0,0 +1,52 @@
+{% extends "admin_base.tpl" %}
+
+{% block title %}{_ Statistics _}{% endblock %}
+
+{% block head_extra %}
+ {% lib
+ "js/d3.js"
+ "js/charts/z_charts.js"
+ "js/charts/histogram-duration.js"
+ "js/charts/line-chart.js"
+ "js/charts/stats_charts.js"
+ "css/charts.css"
+ %}
+{% endblock %}
+
+{% block content %}
+
+ <div class="edit-header">
+ <h2>{_ Site Statistics _}</h2>
+ {% button text="start"
+ action={script script="
+ if (!updateTimer) z_notify('update_metrics')"
+ }
+ %}
+ {% button text="stop"
+ action={script script="
+ clearTimeout(updateTimer); updateTimer = null"
+ }
+ %}
+ </div>
+
+ <div id="test"></div>
+ <div id="graphs"></div>
+
+ {% wire name='new_metrics'
+ action={script
+ script="d3.select('#graphs').call(
+ z_charts.update, zEvtArgs, factory);
+ updateTimer = setTimeout(
+ \"z_notify('update_metrics')\", 2000)"
+ }
+ %}
+
+ {% wire
+ action={script
+ script="var factory = stats_chart_factory();
+ var updateTimer;
+ z_notify('update_metrics');"
+ }
+ %}
+
+ {% endblock %}
View
5 rebar.config
@@ -25,6 +25,9 @@
{mochiweb, ".*", {git, "git://github.com/zotonic/mochiweb.git", "master"}},
{ua_classifier, ".*", {git, "git://github.com/zotonic/ua_classifier.git", "master"}},
{webzmachine, ".*", {git, "git://github.com/zotonic/webzmachine.git", "master"}},
- {z_stdlib, ".*", {git, "git://github.com/zotonic/z_stdlib.git", "master"}}
+ {z_stdlib, ".*", {git, "git://github.com/zotonic/z_stdlib.git", "master"}},
+ {bear, ".*", {git, "git://github.com/boundary/bear.git", "master"}},
+ {folsom, ".*", {git, "git://github.com/boundary/folsom.git", "master"}},
+ {meck, ".*", {git, "git://github.com/eproxus/meck.git", "master"}}
]
}.
View
5 src/dbdrivers/postgresql/z_db.erl
@@ -192,8 +192,11 @@ with_connection(F, Context) ->
with_connection(F, none, _Context) ->
F(none);
with_connection(F, Connection, Context) when is_pid(Connection) ->
+ Counter = #counter{name=requests},
+ From = [#stats_from{system=db}, #stats_from{host=Context#context.host, system=db}],
+ z_stats:update(Counter, From),
try
- F(Connection)
+ z_stats:timed_update(duration, F, [Connection], From)
after
return_connection(Connection, Context)
end.
View
78 src/models/m_stats.erl
@@ -0,0 +1,78 @@
+%% @author Andreas Stenius <git@astekk.se>
+%% @copyright 2013 Andreas Stenius
+%% Date: 2013-02-22
+%%
+%% @doc Model for the zotonic site configuration
+
+%% Copyright 2013 Andreas Stenius
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+
+-module(m_stats).
+-author("Andreas Stenius <git@astekk.se").
+
+-behaviour(gen_model).
+
+%% interface functions
+-export([
+ m_find_value/3,
+ m_to_list/2,
+ m_value/2
+ ]).
+
+-include_lib("zotonic.hrl").
+
+%% @doc Fetch the value for the key from a model source
+%% @spec m_find_value(Key, Source, Context) -> term()
+m_find_value(metrics, #m{ value=undefined }, Context) ->
+ get_metrics(Context);
+m_find_value(metric, #m{ value=undefined }=M, _Context) ->
+ M#m{ value=metric };
+m_find_value(Key, #m{ value=undefined }=M, _Context) ->
+ M#m{ value={key, Key} };
+m_find_value(System, #m{ value=metric }=M, _Context) ->
+ M#m{ value={metric, System} };
+m_find_value(Name, #m{ value={metric, System} }=M, #context{ host=Host }) ->
+ M#m{ value={key, {Host, System, Name}} };
+m_find_value(system, #m{ value={key, {_, System, _}} }, _Context) ->
+ System;
+m_find_value(name, #m{ value={key, {_, _, Name}} }, _Context) ->
+ Name;
+m_find_value(type, #m{ value={key, Key} }, _Context) ->
+ get_metric_type(Key);
+m_find_value(value, #m{ value={key, Key} }, _Context) ->
+ get_metric_value(Key, get_metric_type(Key)).
+
+%% @doc Transform a m_config value to a list, used for template loops
+%% @spec m_to_list(Source, Context) -> All
+m_to_list(#m{ value=undefined }, Context) ->
+ get_metrics(Context).
+
+%% @doc Transform a model value so that it can be formatted or piped through filters
+%% @spec m_value(Source, Context) -> term()
+m_value(#m{ value=undefined }, _Context) ->
+ undefined.
+
+
+get_metrics(#context{ host=Host }) ->
+ M = #m{ model=?MODULE },
+ [M#m{ value={key, Key} } || {H,_,_}=Key <- folsom_metrics:get_metrics(), H==Host].
+
+get_metric_type(Key) ->
+ [{Key, [{type, Type}]}] = folsom_metrics:get_metric_info(Key),
+ Type.
+
+get_metric_value(Key, histogram) ->
+ folsom_metrics:get_histogram_statistics(Key);
+get_metric_value(Key, _) ->
+ folsom_metrics:get_metric_value(Key).
View
7 src/support/z_site_sup.erl
@@ -38,6 +38,13 @@ start_link(Host) ->
%% @spec init(Host) -> SupervisorTree
%% @doc Supervisor callback, returns the supervisor tree for a zotonic site
init(Host) ->
+ HostStats = #stats_from{host=Host},
+ z_stats:new(#counter{name=requests}, HostStats#stats_from{system=webzmachine}),
+ z_stats:new(#counter{name=requests}, HostStats#stats_from{system=db}),
+ z_stats:new(#counter{name=out}, HostStats#stats_from{system=webzmachine}),
+ z_stats:new(#histogram{name=duration}, HostStats#stats_from{system=webzmachine}),
+ z_stats:new(#histogram{name=duration}, HostStats#stats_from{system=db}),
+
% On (re)start we use the newest site config.
SiteProps = z_sites_manager:get_site_config(Host),
View
7 src/support/z_sites_dispatcher.erl
@@ -74,8 +74,10 @@ dispatch(Host, Path, ReqData) ->
Protocol = case wrq:is_ssl(ReqData) of true -> https; false -> http end,
% Find a matching dispatch rule
DispReq = #dispatch{host=Host, path=Path, method=Method, protocol=Protocol},
+ z_stats:update(#counter{name=requests}, #stats_from{system=webzmachine}),
case gen_server:call(?MODULE, DispReq) of
{no_dispatch_match, MatchedHost, NonMatchedPathTokens, Bindings} when MatchedHost =/= undefined ->
+ z_stats:update(#counter{name=requests}, #stats_from{system=webzmachine, host=MatchedHost}),
{ok, ReqDataHost} = webmachine_request:set_metadata(zotonic_host, MatchedHost, ReqDataUA),
Context = case lists:keyfind(z_language, 1, Bindings) of
@@ -125,6 +127,7 @@ dispatch(Host, Path, ReqData) ->
end;
{redirect, MatchedHost} ->
+ z_stats:update(#counter{name=requests}, #stats_from{system=webzmachine, host=MatchedHost}),
RawPath = wrq:raw_path(ReqDataUA),
Uri = z_context:abs_url(RawPath, z_context:new(MatchedHost)),
{handled, redirect(true, z_convert:to_list(Uri), ReqDataUA)};
@@ -133,6 +136,7 @@ dispatch(Host, Path, ReqData) ->
{handled, redirect(false, z_convert:to_list(NewProtocol), NewHost, ReqDataUA)};
{Match, MatchedHost} ->
+ z_stats:update(#counter{name=requests}, #stats_from{system=webzmachine, host=MatchedHost}),
{ok, ReqDataHost} = webmachine_request:set_metadata(zotonic_host, MatchedHost, ReqDataUA),
{Match, ReqDataHost};
@@ -162,7 +166,8 @@ get_host_for_domain(Domain) ->
%% {stop, Reason}
%% @doc Initiates the server.
init(_Args) ->
- {ok, #state{rules=collect_dispatchrules(), fallback_site=z_sites_manager:get_fallback_site()}}.
+ {ok, #state{rules=collect_dispatchrules(),
+ fallback_site=z_sites_manager:get_fallback_site()}}.
%% @spec handle_call(Request, From, State) -> {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
View
7 src/support/z_sites_sup.erl
@@ -42,6 +42,13 @@ start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
+ z_stats:new(#counter{name=requests}, #stats_from{system=webzmachine}),
+ z_stats:new(#counter{name=requests}, #stats_from{system=db}),
+ z_stats:new(#counter{name=out}, #stats_from{system=webzmachine}),
+ z_stats:new(#histogram{name=duration}, #stats_from{system=webzmachine}),
+ z_stats:new(#histogram{name=duration}, #stats_from{system=db}),
+
+
% Sites supervisor, starts all enabled sites
SitesManager = {z_sites_manager,
{z_sites_manager, start_link, []},
View
120 src/support/z_stats.erl
@@ -0,0 +1,120 @@
+%% @author Maas-Maarten Zeeman <mmzeema@xs4all.nl>
+%% @copyright 2013 Maas-Maarten Zeeman
+%% Date: 2013-02-17
+%% @doc Server for matching the request path to correct site and dispatch rule.
+
+%% Copyright 2013 Maas-Maarten Zeeman
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+
+-module(z_stats).
+
+-include_lib("zotonic.hrl").
+-include_lib("webmachine_logger.hrl").
+
+-export([
+ init/0,
+ new/2,
+ update/2,
+ timed_update/3, timed_update/4, timed_update/5]).
+
+%% Act as a webmachine logger.
+-export([log_access/1]).
+
+%% @doc Initialize the statistics collection machinery.
+%%
+init() ->
+ folsom:start().
+
+%% @doc Create a new counters and histograms.
+%%
+new(Stat, From) ->
+ Key = key(Stat, From),
+ case Stat of
+ #counter{} ->
+ folsom_metrics:new_meter(Key);
+ #histogram{} ->
+ folsom_metrics:new_histogram(Key, exdec)
+ end,
+ folsom_metrics:tag_metric(Key, tag(From)).
+
+%% @doc Update a counter, histogram, whatever.
+%%
+update(What, StatsFrom) when is_tuple(StatsFrom) ->
+ update_metric(What, StatsFrom);
+update(_Stat, []) ->
+ ok;
+update(Stat, [H|T]) ->
+ update(Stat, H),
+ update(Stat, T).
+
+%% @doc Execute the function, and store the measured execution time.
+%%
+timed_update(Name, Fun, StatsFrom) ->
+ {Time, Result} = timer:tc(Fun),
+ update(#histogram{name=Name, value=Time}, StatsFrom),
+ Result.
+
+timed_update(Name, Fun, Args, StatsFrom) ->
+ {Time, Result} = timer:tc(Fun, Args),
+ update(#histogram{name=Name, value=Time}, StatsFrom),
+ Result.
+
+timed_update(Name, Mod, Fun, Args, StatsFrom) ->
+ {Time, Result} = timer:tc(Mod, Fun, Args),
+ update(#histogram{name=Name, value=Time}, StatsFrom),
+ Result.
+
+%% @doc Collect log data from webzmachine.
+%%
+log_access(#wm_log_data{start_time=undefined}) ->
+ ok;
+log_access(#wm_log_data{end_time=undefined}) ->
+ ok;
+log_access(#wm_log_data{start_time=StartTime, end_time=EndTime, response_length=ResponseLength}=LogData) ->
+ try
+ Duration = #histogram{name=duration, value=timer:now_diff(EndTime, StartTime)},
+ Out = #counter{name=out, value=ResponseLength},
+ System = #stats_from{system=webzmachine},
+
+ %% The request has already been counted by z_sites_dispatcher.
+ For = case webmachine_logger:get_metadata(zotonic_host, LogData) of
+ undefined ->
+ System;
+ Host ->
+ [System, System#stats_from{host=Host}]
+ end,
+
+ update(Duration, For),
+ update(Out, For)
+ after
+ % Pass it to the default webmachine logger.
+ webmachine_logger:log_access(LogData)
+ end.
+
+
+
+%% Some helper functions.
+
+update_metric(#counter{op=incr, value=Value}=Stat, From) ->
+ folsom_metrics:notify(key(Stat, From), Value);
+update_metric(#histogram{value=Value}=Stat, From) ->
+ folsom_metrics:notify({key(Stat, From), Value}).
+
+key(#counter{name=Name}, #stats_from{host=Host, system=System}) ->
+ {Host, System, Name};
+key(#histogram{name=Name}, #stats_from{host=Host, system=System}) ->
+ {Host, System, Name}.
+
+tag(#stats_from{host=Host}) ->
+ Host.
View
8 src/zotonic_sup.erl
@@ -163,6 +163,7 @@ init([]) ->
false -> Processes ++ IPv4Proc
end,
+ init_stats(),
init_ua_classifier(),
init_webmachine(),
@@ -184,6 +185,11 @@ init([]) ->
{ok, {{one_for_one, 1000, 10}, Processes1}}.
+%% @doc Initializes the stats collector.
+%%
+init_stats() ->
+ z_stats:init().
+
%% @doc Initializes the ua classifier. When it is enabled it is loaded and
%% tested if it works.
init_ua_classifier() ->
@@ -216,7 +222,7 @@ init_webmachine() ->
LogDir = z_config:get_dirty(log_dir),
- application:set_env(webzmachine, webmachine_logger_module, webmachine_logger),
+ application:set_env(webzmachine, webmachine_logger_module, z_stats),
webmachine_sup:start_logger(LogDir),
case z_config:get_dirty(enable_perf_logger) of

0 comments on commit 19ebc17

Please sign in to comment.
Something went wrong with that request. Please try again.