Skip to content

Instantly share code, notes, and snippets.

@mumoshu
Last active December 19, 2015 07:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mumoshu/5918073 to your computer and use it in GitHub Desktop.
Save mumoshu/5918073 to your computer and use it in GitHub Desktop.
Play framework 2.1.0で開発したアプリのメトリクスをGangliaでモニタリングする

Idea

            メトリクス                           Web API(JSON)                                    メトリクス
Play2アプリ ----------> guardian-management-play ------------> Rubyスクリプト --> gmetricコマンド ----------> Webサーバ内のgmond --------> 集約先のgmond/gmetad
  • guardian-management-playはPlay2のフィルタを使ってメトリクスを計測し、管理Web経由で計測データをJSONファイルとして公開する

Play2アプリ側の作業

Play2アプリでMetricsを計測して、それをJSONファイルとしてWeb API経由で提供するところまでやる。 後にRubyスクリプトを使ってこのAPIをポーリングして、結果を定期的にGangliaへ送信します。

  • guardian-management-playというプラグインを使います。
  • 以下をプロジェクトのビルド定義に追加
resolvers += "Guardian Github Snapshots" at "http://guardian.github.com/maven/repo-releases"
libraryDependencies += "com.gu" %% "management-play" % "5.21"

Ganglia側の作業

Mac OS XでGangliaを起動して管理Webを見られるようにするまで

  • Gangliaをインストールする
  • Mac OS Xならbrew install ganglia。なんか失敗したらbrew updateしてからもう一回。
  • gstatが既にあるよ!って言われたらbrew link --overwrite --dry-run gangliaしてgstatだけ上書きされることを確認して、brew link --overwrite gangliaで上書きしちゃえ〜)

mod_phpの組み込みもお忘れなく。httpd.confに以下のような設定を追加。

LoadModule php5_module    /usr/local/Cellar/php54/5.4.16/libexec/apache2/libphp\
5.so

<IfModule mod_php5.c>
    AddType  application/x-httpd-php         .php .php4 .php3 .phtml
    AddType  application/x-httpd-php-source  .phps
    DirectoryIndex index.html index.php
</IfModule>

次に、gmond, gmetadのログをtailしておく。

$ syslog -w

起動ォ

$ sudo gmond
$ sudo mkdir -p /var/lib/ganglia/rrds
$ sudo chown -R nobody:nobody /var/lib/ganglia
$ sudo gmetad

gmondはどのportでLISTENしてるのかな?

$ gmond --default_config | grep -nC 3 port
(省略)
49-udp_recv_channel {
50-  mcast_join = 239.2.11.71
51:  port = 8649
52-  bind = 239.2.11.71
53-}
54-
--
--
55-/* You can specify as many tcp_accept_channels as you like to share
56-   an xml description of the state of the cluster */
57-tcp_accept_channel {
58:  port = 8649
59-}
(省略)

8649っぽい。そして、マルチキャストアドレス 239.2.11.71でマルチキャストするらしい。

管理Webを起動します。

まずApacheとPHPをインストールしておきます。まだインストールしていない場合、こちらを参考に。phpはこちらのhomebrew-phpのほうが簡単です。

以下にGangliaの管理WebのPHPソースがあります。

$ ls /usr/local/Cellar/ganglia/3.1.7/share/ganglia/web/

この内容をDocumentRootへgangliaという名前でシンボリックリンクさせて、Apacheを起動したいと思います。 そうすると、http://localhost/gangliaでアクセスできるはず。

DocumentRootを調べるために、まず、httpd.confの場所を特定します。

$ httpd -V | grep httpd.conf
 -D SERVER_CONFIG_FILE="/usr/local/etc/apache2/httpd.conf"

DocumentRootを調べます。

$ grep DocumentRoot /usr/local/etc/apache2/httpd.conf
# DocumentRoot: The directory out of which you will serve your
DocumentRoot "/usr/local/Cellar/httpd/2.2.23/share/apache2/htdocs"
# This should be changed to whatever you set DocumentRoot to.
    # access content that does not live under the DocumentRoot.

DocumentRootへGangliaの管理WebのPHPソースへシンボリックリンクを貼ります。

$ cd /usr/local/Cellar/httpd/2.2.23/share/apache2/htdocs
$ ln -s /usr/local/Cellar/ganglia/3.1.7/share/ganglia/web ganglia
$ ls
ganglia  	index.html
$ sudo apachectl start

http://localhost/gangliaへアクセスします。

管理Webが表示されたらOK。

メトリクスをGangliaへ送信する

gmetricの道通確認

メトリクスがGangliaへ送信されるためには、以下が送信側(gmetricコマンド, gmetric4j)と受信側(gmond)で一致している必要がある。

  • 通信方法(unicast or multicast)
  • unicast/multicastの送信先アドレスとポート
  • gmetricプロトコルのバージョン

受信側に問題がないか確認するため、gmetricコマンドで適当なメトリクスを送信してみる。

$ gmetric -n "sample_int8" -v "1" -t int8

この例では、sample_int8という名前のメトリクスを送信した。数秒後、同盟のrrdファイルが/var/lig/ganglia/rrdsディレクトリに作成されていれば、正しく送受信できたといえる。findコマンドで確認してみる。

$ find /var/lib/ganglia/rrds | grep sample
/var/lib/ganglia/rrds/__SummaryInfo__/sample_int8.rrd
/var/lib/ganglia/rrds/unspecified/10.128.56.4/sample_int8.rrd
/var/lib/ganglia/rrds/unspecified/__SummaryInfo__/sample_int8.rrd

スクリプトでメトリクスのJSONを取得、変換してGangliaへ送信する

guardian-management-playの管理Webが提供するJSONの内容をgmetricコマンドで送信する。 このgistにあるjson_stats_fetcher.rbスクリプトを使う。 実際には、これをcronなどで定期的に実行するとよい。

参考情報

package conf
import com.gu.management._
import com.gu.management.logback._
import play.RequestMetrics
// example of creating your own new page type
class DummyPage extends ManagementPage {
val path = "/management/dummy"
def get(request: HttpRequest) = PlainTextResponse("Hello dummy!")
}
// switches
object Switches {
val omniture = new DefaultSwitch("omniture", "enables omniture java script")
val takeItDown = new DefaultSwitch("take-it-down", "enable this switch to take the site down", initiallyOn = false)
val all = List(omniture, takeItDown, Healthcheck.switch)
}
object PlayExampleRequestMetrics extends RequestMetrics.Standard
// properties
object Properties {
// If I were using com.gu.configuration I'd comment out the following line
// val all = new ConfigurationFactory getConfiguration ("music", "conf/arts_music").toString
val all = "key1=value1\nkey2=value2"
}
object Management extends com.gu.management.play.Management {
val applicationName: String = "Example Play App"
lazy val pages = List(
new DummyPage(),
new ManifestPage(),
new Switchboard(applicationName, Switches.all),
StatusPage(applicationName, ExceptionCountMetric :: ServerErrorCounter :: ClientErrorCounter :: PlayExampleRequestMetrics.asMetrics),
new HealthcheckManagementPage(),
new PropertiesPage(Properties.all),
new LogbackLevelPage(applicationName)
)
}
import conf.PlayExampleRequestMetrics
import play.api.mvc.WithFilters
object Global extends WithFilters(PlayExampleRequestMetrics.asFilters: _*)
1000:com.gu.management.play.InternalManagementPlugin
*/1 * * * * /Users/yusuke.kuoka/Projects/play2-ganglia-example/json_stats_fetcher
#!/usr/bin/env bash
# load rvm ruby
source /usr/local/rvm/environments/ruby-2.0.0-p195@global
ruby json_stats_fetcher.rb 2>&1 | tee /tmp/json_stats_fetcher.log
#!/usr/bin/ruby
# json_stats_fetcher.rb - Publish guardian-management-play stats to Ganglia.
#
# The latest version is always available at:
# https://gist.github.com/mumoshu/5918073
#
# Tested on Ruby 2.0.0-p195 and 1.9.3-p429
require 'rubygems'
require 'getoptlong'
require 'socket'
require 'json'
require 'timeout'
require 'open-uri'
$report_to_ganglia = true
$ganglia_prefix = ''
$ganglia_group = ''
$stat_timeout = 5*60
$pattern = /^x-/
hostname = "localhost"
# The default port for guardian-management-play
port = 18080
use_web = true
conf = '/Users/yusuke.kuoka/gmond.conf'
def usage(port)
puts
puts "usage: json_stats_fetcher.rb [options]"
puts "options:"
puts " -n say what I would report, but don't report it"
puts " -w use web interface"
puts " -h <hostname> connect to another host (default: localhost)"
puts " -i <pattern> ignore all stats matching pattern (default: #{$pattern.inspect})"
puts " -p <port> connect to another port (default: #{port})"
puts " -P <prefix> optional prefix for ganglia names"
puts " -g <group> optional ganglia group"
puts
end
opts = GetoptLong.new(
[ '--help', GetoptLong::NO_ARGUMENT ],
[ '-n', GetoptLong::NO_ARGUMENT ],
[ '-h', GetoptLong::REQUIRED_ARGUMENT ],
[ '-i', GetoptLong::REQUIRED_ARGUMENT ],
[ '-p', GetoptLong::REQUIRED_ARGUMENT ],
[ '-P', GetoptLong::REQUIRED_ARGUMENT ],
[ '-g', GetoptLong::REQUIRED_ARGUMENT ],
[ '-t', GetoptLong::REQUIRED_ARGUMENT ],
[ '-w', GetoptLong::NO_ARGUMENT ],
[ '-c', GetoptLong::REQUIRED_ARGUMENT]
)
opts.each do |opt, arg|
case opt
when '--help'
usage(port)
exit 0
when '-n'
$report_to_ganglia = false
when '-h'
hostname = arg
when '-i'
$pattern = /#{arg}/
when '-p'
port = arg.to_i
when '-P'
$ganglia_prefix = arg
when '-g'
$ganglia_group = arg
when '-w'
port = 9990
use_web = true
when '-c'
conf = arg
end
end
stats_dir = "/tmp/stats-#{port}"
singleton_file = "#{stats_dir}/json_stats_fetcher_running"
Dir.mkdir(stats_dir) rescue nil
if File.exist?(singleton_file)
puts "NOT RUNNING -- #{singleton_file} exists."
puts "Kill other stranded stats checker processes and kill this file to resume."
exit 1
end
File.open(singleton_file, "w") { |f| f.write("i am running.\n") }
## we will accumulate all our metrics in here
metrics = []
begin
Timeout::timeout(55) do
data = if use_web
url = "http://#{hostname}:#{port}/management/status"
puts "Opening #{url}..."
open(url).read
else
socket = TCPSocket.new(hostname, port)
socket.puts("stats/json#{' reset' if $report_to_ganglia}")
socket.gets
end
stats = JSON.parse(data)
metrics = []
class TypeDef
attr :keys
def initialize(keys)
@keys = keys
end
end
for m in stats["metrics"] do
type_def_for = {
"timer" => TypeDef.new(["count", "totalTime"]),
"counter" => TypeDef.new(["count"]),
"gauge" => TypeDef.new(["value"])
}
group, name, type, title, description = m.values_at("group", "name", "type", "title", "description")
type_def = type_def_for[type]
if type_def.nil?
raise "Unexpected type `#{type}` found. Expected one of #{type_def_for.values.map{|type| "'" + type | "'"}.join(', ')}."
end
for k in type_def.keys
value = m[k]
units = k
metrics << ["play2_#{group}_#{name}", value.to_i, units]
end
if type == 'timer'
totalTime = m['totalTime'].to_f
count = m['count'].to_f
if count > 0
metrics << ["play2_#{group}_#{name}_avg", totalTime / count, 'milliseconds']
end
end
end
end
## do stuff with the metrics we've accumulated
## first, munge metric names
metrics = metrics.map do |name, value, units|
# Ganglia is very intolerant of metric named with non-standard characters,
# where non-standard contains most everything other than letters, numbers and
# some common symbols.
name = name.gsub(/[^A-Za-z0-9_\-\.]/, "_")
[name, value, units]
end
## now, send to ganglia or print to $stdout
if $report_to_ganglia # call gmetric for metrics
metrics.each_slice(100) do |some_metrics|
cmd = some_metrics.map do |name, value, units|
"gmetric -t float -n \"#{$ganglia_prefix}#{name}\" -v \"#{value}\" -u \"#{units}\" -d #{$stat_timeout}"
end.join("\n")
puts "Running #{cmd}..."
res = system cmd
unless res
STDERR.puts "system(gmetric) failed: status:#{$?.exitstatus}"
end
end
else # print a report to stdout
report = metrics.map do |name, value, units|
"#{$ganglia_group}/#{$ganglia_prefix}#{name}=#{value}"
end.join("\n")
puts report
end
ensure
File.unlink(singleton_file)
end
import sbt._
import Keys._
import play.Project._
object ApplicationBuild extends Build {
val appName = "play2-ganglia-example"
val appVersion = "1.0-SNAPSHOT"
val appDependencies = Seq(
// Add your project dependencies here,
jdbc,
anorm
)
val main = play.Project(appName, appVersion, appDependencies).settings(
resolvers += "Guardian Github Snapshots" at "http://guardian.github.com/maven/repo-releases",
libraryDependencies += "com.gu" %% "management-play" % "5.26"
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment