├── data
└── .gitkeep
├── views
├── docs
│ └── api.slim
├── dump.slim
├── index.slim
├── layout.slim
├── slow_query.slim
├── list.slim
├── dumped_query.slim
└── view.slim
├── .rspec
├── config.sample.toml
├── lib
├── nata2.rb
└── nata2
│ ├── version.rb
│ ├── config.rb
│ ├── helpers.rb
│ ├── mysqldumpslow.rb
│ ├── data.rb
│ └── server.rb
├── Gemfile
├── public
├── fonts
│ ├── glyphicons-halflings-regular.eot
│ ├── glyphicons-halflings-regular.ttf
│ ├── glyphicons-halflings-regular.woff
│ └── glyphicons-halflings-regular.svg
├── css
│ ├── morris.css
│ ├── prettify-tommorow.css
│ └── bootstrap-theme.min.css
└── js
│ ├── lang-sql.js
│ ├── prettify.js
│ ├── bootstrap.min.js
│ └── morris.min.js
├── Rakefile
├── config.ru
├── spec
├── nata2
│ ├── data_spec.rb
│ └── server_spec.rb
└── spec_helper.rb
├── .travis.yml
├── bin
└── migrate_data_for_1_0_0
├── .gitignore
├── nata2.gemspec
├── Schemafile
└── README.md
/data/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/views/docs/api.slim:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --colour --format d
2 |
--------------------------------------------------------------------------------
/config.sample.toml:
--------------------------------------------------------------------------------
1 | dburl = "sqlite://data/nata2.db"
2 |
--------------------------------------------------------------------------------
/lib/nata2.rb:
--------------------------------------------------------------------------------
1 | require 'nata2/version'
2 |
3 | module Nata2
4 | end
5 |
--------------------------------------------------------------------------------
/lib/nata2/version.rb:
--------------------------------------------------------------------------------
1 | module Nata2
2 | VERSION = '1.0.0'
3 | end
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in nata2.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/public/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/studio3104/nata2/HEAD/public/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/public/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/studio3104/nata2/HEAD/public/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/gem_tasks'
2 | require 'rspec/core/rake_task'
3 |
4 | RSpec::Core::RakeTask.new('spec')
5 | task :default => :spec
6 |
--------------------------------------------------------------------------------
/public/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/studio3104/nata2/HEAD/public/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
2 | require 'sinatra'
3 | require 'nata2/server'
4 |
5 | run Nata2::Server
6 |
--------------------------------------------------------------------------------
/spec/nata2/data_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'nata2/data'
3 |
4 | describe Nata2::Data do
5 | let(:data) { Nata2::Data.new }
6 | end
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 |
3 | rvm:
4 | - 2.0.0
5 | - 2.1.1
6 |
7 | before_install:
8 | - gem update bundler
9 |
10 | script:
11 | - bundle exec rake spec
12 |
--------------------------------------------------------------------------------
/bin/migrate_data_for_1_0_0:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
3 | require 'nata2/data'
4 | data = Nata2::Data.new
5 | data.migrate_data_for_1_0_0
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | *.db
4 | .bundle
5 | .config
6 | config.toml
7 | .yardoc
8 | Gemfile.lock
9 | InstalledFiles
10 | _yardoc
11 | coverage
12 | doc/
13 | lib/bundler/man
14 | pkg
15 | rdoc
16 | spec/reports
17 | test/tmp
18 | test/version_tmp
19 | tmp
20 |
--------------------------------------------------------------------------------
/public/css/morris.css:
--------------------------------------------------------------------------------
1 | .morris-hover{position:absolute;z-index:1000;}.morris-hover.morris-default-style{border-radius:10px;padding:6px;color:#666;background:rgba(255, 255, 255, 0.8);border:solid 2px rgba(230, 230, 230, 0.8);font-family:sans-serif;font-size:12px;text-align:center;}.morris-hover.morris-default-style .morris-hover-row-label{font-weight:bold;margin:0.25em 0;}
2 | .morris-hover.morris-default-style .morris-hover-point{white-space:nowrap;margin:0.1em 0;}
3 |
--------------------------------------------------------------------------------
/lib/nata2/config.rb:
--------------------------------------------------------------------------------
1 | require 'nata2'
2 | require 'toml'
3 |
4 | config_file = File.expand_path('config.toml', "#{__dir__}/../..")
5 | CONFIG = File.exist?(config_file) ? TOML.load_file(config_file) : {}
6 |
7 | class Nata2::Config
8 | @@dburl = nil
9 | def self.get(keyword)
10 | case keyword
11 | when :dburl
12 | return @@dburl if @@dburl
13 | if ENV['RACK_ENV'] == 'test'
14 | require 'tempfile'
15 | temp = Tempfile.new('nata2test.db')
16 | @@dburl = %Q{sqlite://#{temp.path}}
17 | else
18 | @@dburl = CONFIG['dburl'] || %Q[sqlite://#{File.dirname(__FILE__)}/../../data/nata2.db]
19 | end
20 | else
21 | raise ArgumentError, "unknown configuration keyword: #{keyword}"
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/views/dump.slim:
--------------------------------------------------------------------------------
1 | p style="float: right;" [ 合計値 (平均値) ]
2 | table.table.table-hover.table-condensed
3 | tr
4 | th width="54%;" Query
5 | th.text-right width="2%;" count
6 | th.text-right width="12%;" query time
7 | th.text-right width="9%;" lock time
8 | th.text-right width="13%;" rows sent
9 |
10 | - @slow_queries.each do |query|
11 | tr
12 | td
13 | a target="_blank" href="/dumped_query/#{Base64.strict_encode64(JSON.generate(query))}"
14 | - cut_sql = query[:normarized_sql].gsub(/\n/, ' ').gsub(/\s+/, ' ')
15 | - cut_sql = cut_sql[0, 90] + '...' if cut_sql.bytesize > 90
16 | #{cut_sql}
17 | td align="right"
18 | #{query[:count]}
19 | td align="right"
20 | #{sprintf("%0.2f", query[:summation][:query_time])} (#{sprintf("%0.2f", query[:average][:query_time])}) s
21 | td align="right"
22 | #{sprintf("%0.2f", query[:summation][:lock_time])} (#{sprintf("%0.2f", query[:average][:lock_time])}) s
23 | td align="right"
24 | #{query[:summation][:rows_sent]} (#{sprintf("%0.2f", query[:average][:rows_sent])})
25 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RACK_ENV'] = 'test'
2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3 | require 'nata2/server'
4 | require 'nata2/data'
5 | require 'nata2/config'
6 | require 'uri'
7 | require 'rspec'
8 | require 'rack/test'
9 |
10 | module RSpecMixin
11 | include Rack::Test::Methods
12 | include Nata2::Helpers
13 | def app() Nata2::Server end
14 | end
15 |
16 | RSpec.configure do |c|
17 | c.include RSpecMixin
18 | end
19 |
20 | class TestData
21 | ServiceName = 'nataapplication'
22 | HostName = 'nata.db01'
23 | DatabaseName = 'nata_db'
24 | ParsedSlowQuery = {
25 | datetime: 1390883951, user: 'user', host: 'localhost',
26 | query_time: 2.001227, lock_time: 0.0, rows_sent: 1, rows_examined:0,
27 | sql: 'select sleep(2)'
28 | }
29 | end
30 |
31 | path_to_db = URI.parse(Nata2::Config.get(:dburl)).path
32 | Dir.chdir(File.join(File.dirname(__FILE__), '..')) {
33 | system(%Q[bundle exec ridgepole -c "{adapter: sqlite3, database: #{path_to_db}}" --apply])
34 | }
35 | data = Nata2::Data.new
36 | data.register_slow_query(TestData::ServiceName, TestData::HostName, TestData::DatabaseName, TestData::ParsedSlowQuery)
37 |
--------------------------------------------------------------------------------
/views/index.slim:
--------------------------------------------------------------------------------
1 | - @bundles.each do |service_name, databases|
2 | .panel-group id="accordion_#{service_name}"
3 | .panel.panel-default
4 | .panel-heading
5 | h4.panel-title
6 | a data-toggle="collapse" data-parent="#accordion_#{service_name}" href="#collapse_#{service_name}"
7 | span.glyphicon.glyphicon-collapse-down
8 | span #{service_name}
9 | .panel-collapse.collapse.in id="collapse_#{service_name}"
10 | .panel-body
11 | - if !@complex[service_name].empty?
12 | b
13 | | Complex
14 | ul.list-unstyled
15 | - @complex[service_name].each do |database_name|
16 | li
17 | span
18 | | ●
19 | span
20 | a href="/view_complex/#{service_name}/#{database_name}" #{database_name}
21 |
22 | b
23 | | Database
24 | ul.list-unstyled
25 | - databases.each do |database|
26 | li
27 | span
28 | font color="#{database[:color]}" ●
29 | span
30 | |
31 | span
32 | a href="/view/#{service_name}/#{database[:host]}/#{database[:database]}" #{database[:database]}
33 | span (
34 | span #{database[:host]}
35 | span )
36 |
--------------------------------------------------------------------------------
/views/layout.slim:
--------------------------------------------------------------------------------
1 | doctype html
2 | html lang="en"
3 | head
4 | meta charset="UTF-8"
5 | link rel="stylesheet" type="text/css" href="/css/bootstrap-theme.min.css"
6 | link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css"
7 | link rel="stylesheet" type="text/css" href="/css/prettify-tommorow.css"
8 | link rel="stylesheet" type="text/css" href="/css/morris.css"
9 | css:
10 | body { padding-top: 70px; }
11 |
12 | body
13 | header.navbar.navbar-default.navbar-fixed-top.bs-docs-nav role="banner"
14 | .container
15 | .navbar-header
16 | a.navbar-brand 鉈
17 | nav.collapse.navbar-collapse.bs-navbar-collapse role="navigation"
18 | ul.nav.navbar-nav
19 | li
20 | a href="/" データベース選択
21 | li
22 | li
23 | ul.nav.navbar-nav.navbar-right
24 | li
25 | a href="/docs/api" APIドキュメント
26 |
27 | .container
28 | .row
29 | == yield
30 |
31 | script type="text/javascript" src="/js/jquery.min.js"
32 | script type="text/javascript" src="/js/raphael-min.js"
33 | script type="text/javascript" src="/js/morris.min.js"
34 | script type="text/javascript" src="/js/bootstrap.min.js"
35 | script type="text/javascript" src="/js/prettify.js"
36 | script type="text/javascript" src="/js/lang-sql.js"
37 | javascript:
38 | prettyPrint();
39 |
--------------------------------------------------------------------------------
/views/slow_query.slim:
--------------------------------------------------------------------------------
1 | blockquote style="border-color : #FFFFFF;"
2 | h6
3 | span.label.label-default #{@slow_query[:service_name]}
4 | |
5 | a href="/view/#{@slow_query[:service_name]}/#{@slow_query[:host_name]}/#{@slow_query[:database_name]}"
6 | span.label style="background-color : #{@slow_query[:color]}" #{@slow_query[:database_name]}(#{@slow_query[:host_name]})
7 | h6
8 | - start_unixtime = (@slow_query[:datetime].to_f - @slow_query[:query_time]).ceil
9 | - start_datetime = Time.at(start_unixtime).strftime('%Y/%m/%d %H:%M:%S')
10 | - end_datetime = Time.at(@slow_query[:datetime]).strftime('%Y/%m/%d %H:%M:%S')
11 | span
12 | b #{@slow_query[:user]} @ #{@slow_query[:host]}
13 | span style="float: right;"
14 | b #{start_datetime} - #{end_datetime}
15 | h6
16 | table.table.table-striped.table-condensed
17 | tr
18 | th.text-right width="25%;" query time
19 | th.text-right width="25%;" lock time
20 | th.text-right width="25%;" rows sent
21 | th.text-right width="25%;" rows examined
22 | tr
23 | td align="right" #{sprintf("%0.2f", @slow_query[:query_time])} s
24 | td align="right" #{sprintf("%0.2f", @slow_query[:lock_time])} s
25 | td align="right" #{@slow_query[:rows_sent]}
26 | td align="right" #{@slow_query[:rows_examined]}
27 | pre.prettyprint.lang-sql
28 | #{@slow_query[:sql]}
29 |
--------------------------------------------------------------------------------
/nata2.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'nata2/version'
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = 'nata2'
8 | spec.version = Nata2::VERSION
9 | spec.authors = ['studio3104']
10 | spec.email = ['studio3104.com@gmail.com']
11 | spec.summary = %q{Analyzer of MySQL slow query log}
12 | spec.description = %q{Analyzer of MySQL slow query log}
13 | spec.homepage = ''
14 | spec.license = 'MIT'
15 |
16 | spec.files = `git ls-files -z`.split("\x0")
17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19 | spec.require_paths = ['lib']
20 |
21 | spec.add_development_dependency 'bundler', '~> 1.5'
22 | spec.add_development_dependency 'rake'
23 | spec.add_development_dependency 'rspec'
24 | spec.add_development_dependency 'webmock'
25 | spec.add_runtime_dependency 'sinatra'
26 | spec.add_runtime_dependency 'sinatra-contrib'
27 | spec.add_runtime_dependency 'slim', '= 2.0.2'
28 | spec.add_runtime_dependency 'toml'
29 | spec.add_runtime_dependency 'sequel'
30 | spec.add_runtime_dependency 'sqlite3'
31 | spec.add_runtime_dependency 'mysql2', '~> 0.3.20'
32 | spec.add_runtime_dependency 'ridgepole'
33 | spec.add_runtime_dependency 'focuslight-validator'
34 | end
35 |
--------------------------------------------------------------------------------
/views/list.slim:
--------------------------------------------------------------------------------
1 | - @slow_queries_per_day.each do |day, slow_queries|
2 | h4 #{day}
3 | table.table.table-hover.table-condensed
4 | tr
5 | th width="3%;"
6 | th width="7%;" Time
7 | th width="54%;" Query
8 | th.text-right width="8%;" query time
9 | th.text-right width="8%;" lock time
10 | th.text-right width="10%;" rows sent
11 | th.text-right width="10%;" rows examined
12 |
13 | - slow_queries.each do |query|
14 | tr
15 | td align="center"
16 | a href="/view/#{query[:service_name]}/#{query[:host_name]}/#{query[:database_name]}"
17 | font color="#{query[:color]}" ●
18 | td #{Time.at(query[:datetime]).strftime('%H:%M:%S')}
19 | td
20 | a target="_blank" href='/slow_query/#{query[:id]}'
21 | - cut_sql = query[:sql].gsub(/\n/, ' ').gsub(/\s+/, ' ')
22 | - cut_sql = cut_sql[0, 80] + '...' if cut_sql.bytesize > 80
23 | #{cut_sql}
24 | td align="right" #{sprintf("%0.2f", query[:query_time])}
25 | td align="right" #{sprintf("%0.2f", query[:lock_time])}
26 | td align="right" #{query[:rows_sent]}
27 | td align="right" #{query[:rows_examined]}
28 |
29 | ul.pager
30 | - if @page == 1
31 | li.disabled
32 | a Newer
33 | - else
34 | li
35 | a href="?#{URI.encode_www_form(@params.merge(page: @page-1))}" Newer
36 | - if @disabled_next
37 | li.disabled
38 | a Older
39 | - else
40 | li
41 | a href="?#{URI.encode_www_form(@params.merge(page: @page+1))}" Older
42 |
--------------------------------------------------------------------------------
/public/js/lang-sql.js:
--------------------------------------------------------------------------------
1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/,null,"\"'"]],[["com",/^(?:--[^\n\r]*|\/\*[\S\s]*?(?:\*\/|$))/],["kwd",/^(?:add|all|alter|and|any|apply|as|asc|authorization|backup|begin|between|break|browse|bulk|by|cascade|case|check|checkpoint|close|clustered|coalesce|collate|column|commit|compute|connect|constraint|contains|containstable|continue|convert|create|cross|current|current_date|current_time|current_timestamp|current_user|cursor|database|dbcc|deallocate|declare|default|delete|deny|desc|disk|distinct|distributed|double|drop|dummy|dump|else|end|errlvl|escape|except|exec|execute|exists|exit|fetch|file|fillfactor|following|for|foreign|freetext|freetexttable|from|full|function|goto|grant|group|having|holdlock|identity|identitycol|identity_insert|if|in|index|inner|insert|intersect|into|is|join|key|kill|left|like|lineno|load|match|matched|merge|natural|national|nocheck|nonclustered|nocycle|not|null|nullif|of|off|offsets|on|open|opendatasource|openquery|openrowset|openxml|option|or|order|outer|over|partition|percent|pivot|plan|preceding|precision|primary|print|proc|procedure|public|raiserror|read|readtext|reconfigure|references|replication|restore|restrict|return|revoke|right|rollback|rowcount|rowguidcol|rows?|rule|save|schema|select|session_user|set|setuser|shutdown|some|start|statistics|system_user|table|textsize|then|to|top|tran|transaction|trigger|truncate|tsequal|unbounded|union|unique|unpivot|update|updatetext|use|user|using|values|varying|view|waitfor|when|where|while|with|within|writetext|xml)(?=[^\w-]|$)/i,
2 | null],["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i],["pln",/^[_a-z][\w-]*/i],["pun",/^[^\w\t\n\r "'\xa0][^\w\t\n\r "'+\xa0-]*/]]),["sql"]);
3 |
--------------------------------------------------------------------------------
/views/dumped_query.slim:
--------------------------------------------------------------------------------
1 | blockquote style="border-color : #FFFFFF;"
2 | pre.prettyprint.lang-sql
3 | #{@dumped_query[:normarized_sql]}
4 | h6
5 | table.table.table-bordered.table-striped.table-condensed
6 | tr
7 | th count
8 | th colspan="2" query time
9 | th colspan="2" lock time
10 | th colspan="2" rows sent
11 | th colspan="2" rows examined
12 | tr
13 | td
14 | span
15 | b total
16 | span style="float: right;" #{@dumped_query[:count]}
17 | td width="11%;"
18 | span
19 | b total
20 | span style="float: right;" #{sprintf("%0.2f", @dumped_query[:summation][:query_time])} s
21 | td width="11%;"
22 | span
23 | b avg
24 | span style="float: right;" #{sprintf("%0.2f", @dumped_query[:average][:query_time])} s
25 | td width="11%;"
26 | span
27 | b total
28 | span style="float: right;" #{sprintf("%0.2f", @dumped_query[:summation][:lock_time])} s
29 | td width="11%;"
30 | span
31 | b avg
32 | span style="float: right;" #{sprintf("%0.2f", @dumped_query[:average][:lock_time])} s
33 | td width="11%;"
34 | span
35 | b total
36 | span style="float: right;" #{@dumped_query[:summation][:rows_sent]}
37 | td width="11%;"
38 | span
39 | b avg
40 | span style="float: right;" #{sprintf("%0.2f", @dumped_query[:average][:rows_sent])}
41 | td width="11%;"
42 | span
43 | b total
44 | span style="float: right;" #{@dumped_query[:summation][:rows_examined]}
45 | td width="11%;"
46 | span
47 | b avg
48 | span style="float: right;" #{sprintf("%0.2f", @dumped_query[:average][:rows_examined])}
49 |
--------------------------------------------------------------------------------
/Schemafile:
--------------------------------------------------------------------------------
1 | create_table "bundles", force: :cascade do |t|
2 | t.string "service_name", limit: 255, null: false
3 | t.string "host_name", limit: 255, null: false
4 | t.string "database_name", limit: 255, null: false
5 | t.string "color", limit: 255, null: false
6 | t.integer "created_at", null: false
7 | t.integer "updated_at", null: false
8 | end
9 |
10 | add_index "bundles", ["service_name", "host_name", "database_name"], name: "bundles_dbname_index", unique: true
11 |
12 | create_table "explains", force: :cascade do |t|
13 | t.integer "slow_query_id", limit: 8
14 | t.integer "explain_id", null: false
15 | t.string "select_type", limit: 255, null: false
16 | t.string "table", limit: 255, null: false
17 | t.string "type", limit: 255, null: false
18 | t.string "possible_keys", limit: 255
19 | t.string "key", limit: 255
20 | t.integer "key_len"
21 | t.string "ref", limit: 255
22 | t.integer "rows", limit: 8, null: false
23 | t.text "extra"
24 | t.integer "created_at", null: false
25 | t.integer "updated_at", null: false
26 | end
27 |
28 | create_table "slow_queries", force: :cascade do |t|
29 | t.integer "bundle_id", limit: 8
30 | t.integer "datetime"
31 | t.integer "period_per_hour"
32 | t.integer "period_per_day"
33 | t.string "user", limit: 255
34 | t.string "host", limit: 255
35 | t.float "query_time"
36 | t.float "lock_time"
37 | t.integer "rows_sent", limit: 8
38 | t.integer "rows_examined", limit: 8
39 | t.text "sql"
40 | t.string "explain", limit: 255, default: "none", null: false
41 | t.integer "created_at", null: false
42 | t.integer "updated_at", null: false
43 | end
44 |
45 | add_index "slow_queries", ["datetime"], name: "slow_queries_datetime_index"
46 | add_index "slow_queries", ["period_per_hour"], name: "slow_queries_period_per_hour_index"
47 | add_index "slow_queries", ["period_per_day"], name: "slow_queries_period_per_day_index"
48 |
--------------------------------------------------------------------------------
/public/css/prettify-tommorow.css:
--------------------------------------------------------------------------------
1 | /* Tomorrow Theme */
2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */
3 | .prettyprint {
4 | background: white;
5 | font-family: Menlo, 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Consolas, monospace;
6 | font-size: 12px;
7 | line-height: 1.5;
8 | border: 1px solid #ccc;
9 | padding: 10px;
10 | }
11 |
12 | .pln {
13 | color: #4d4d4c;
14 | }
15 |
16 | @media screen {
17 | .str {
18 | color: #718c00;
19 | }
20 |
21 | .kwd {
22 | color: #8959a8;
23 | }
24 |
25 | .com {
26 | color: #8e908c;
27 | }
28 |
29 | .typ {
30 | color: #4271ae;
31 | }
32 |
33 | .lit {
34 | color: #f5871f;
35 | }
36 |
37 | .pun {
38 | color: #4d4d4c;
39 | }
40 |
41 | .opn {
42 | color: #4d4d4c;
43 | }
44 |
45 | .clo {
46 | color: #4d4d4c;
47 | }
48 |
49 | .tag {
50 | color: #c82829;
51 | }
52 |
53 | .atn {
54 | color: #f5871f;
55 | }
56 |
57 | .atv {
58 | color: #3e999f;
59 | }
60 |
61 | .dec {
62 | color: #f5871f;
63 | }
64 |
65 | .var {
66 | color: #c82829;
67 | }
68 |
69 | .fun {
70 | color: #4271ae;
71 | }
72 | }
73 | @media print, projection {
74 | .str {
75 | color: #006600;
76 | }
77 |
78 | .kwd {
79 | color: #006;
80 | font-weight: bold;
81 | }
82 |
83 | .com {
84 | color: #600;
85 | font-style: italic;
86 | }
87 |
88 | .typ {
89 | color: #404;
90 | font-weight: bold;
91 | }
92 |
93 | .lit {
94 | color: #004444;
95 | }
96 |
97 | .pun, .opn, .clo {
98 | color: #444400;
99 | }
100 |
101 | .tag {
102 | color: #006;
103 | font-weight: bold;
104 | }
105 |
106 | .atn {
107 | color: #440044;
108 | }
109 |
110 | .atv {
111 | color: #006600;
112 | }
113 | }
114 | /* Specify class=linenums on a pre to get line numbering */
115 | ol.linenums {
116 | margin-top: 0;
117 | margin-bottom: 0;
118 | }
119 |
120 | /* IE indents via margin-left */
121 | li.L0,
122 | li.L1,
123 | li.L2,
124 | li.L3,
125 | li.L4,
126 | li.L5,
127 | li.L6,
128 | li.L7,
129 | li.L8,
130 | li.L9 {
131 | /* */
132 | }
133 |
134 | /* Alternate shading for lines */
135 | li.L1,
136 | li.L3,
137 | li.L5,
138 | li.L7,
139 | li.L9 {
140 | /* */
141 | }
142 |
--------------------------------------------------------------------------------
/lib/nata2/helpers.rb:
--------------------------------------------------------------------------------
1 | require 'nata2/config'
2 | require 'focuslight-validator'
3 |
4 | module Nata2::Helpers
5 | def validate(*args)
6 | Focuslight::Validator.validate(*args)
7 | end
8 |
9 | def rule(*args)
10 | Focuslight::Validator.rule(*args)
11 | end
12 |
13 | def data
14 | @data ||= Nata2::Data.new
15 | end
16 |
17 | def config(name)
18 | Nata2::Config.get(name)
19 | end
20 |
21 | def labels(service_name, host_name, database_name)
22 | bundles = data.find_bundles(service_name: service_name, host_name: host_name, database_name: database_name)
23 | labels = {}
24 | bundles.each do |bundle|
25 | name = %Q{#{bundle[:database_name]}(#{bundle[:host_name]})}
26 | labels[name] = { color: bundle[:color], path: %Q{/view/#{bundle[:service_name]}/#{bundle[:host_name]}/#{bundle[:database_name]}} }
27 | end
28 | labels
29 | end
30 |
31 | def from_datetime(time_range)
32 | now = Time.now.to_i
33 | case time_range
34 | when 'd' then now - 86400
35 | when 'w' then now - 86400 * 7
36 | when 'm' then now - 86400 * 30
37 | when 'y' then now - 86400 * 365
38 | else
39 | halt
40 | end
41 | end
42 |
43 | def get_graph_data(service_name, host_name, database_name, time_range)
44 | from = from_datetime(time_range)
45 | graph_data = data.get_slow_queries_count_by_period(
46 | per_day: time_range == 'y', from_datetime: from, service_name: service_name, host_name: host_name, database_name: database_name
47 | )
48 | return [] if graph_data.empty?
49 |
50 | #{"service_name":"service_name","host_name":"host_name2","database_name":"database_name","period":1431082800,"count":4}
51 |
52 | period_column_name, fmt_strftime, plot_per = if time_range == 'y'
53 | [ :period_per_day, '%Y-%m-%d', 3600 * 24 ]
54 | else
55 | [ :period_per_hour, '%Y-%m-%d %H:00', 3600 ]
56 | end
57 |
58 | graph_data = graph_data.to_a.group_by { |gd| gd[period_column_name] }
59 | temp = graph_data.max_by { |_, gd| gd.size }
60 | template = {}
61 | temp.last.each { |tmp|
62 | template.merge!({
63 | %Q{#{tmp[:database_name]}(#{tmp[:host_name]})}.to_sym => 0
64 | })
65 | }
66 | result = []
67 | period = graph_data.min_by { |prd, _| prd }.first
68 | max_period = graph_data.max_by { |prd, _| prd }.first
69 | while period <= max_period do
70 | tgd = template.merge(period: Time.at(period).strftime(fmt_strftime))
71 | graph_data[period].each do |gd|
72 | tgd = tgd.merge({ %Q{#{gd[:database_name]}(#{gd[:host_name]})}.to_sym => gd[:count] })
73 | end
74 | period += plot_per
75 | result << tgd
76 | end
77 | return result
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/lib/nata2/mysqldumpslow.rb:
--------------------------------------------------------------------------------
1 | require 'nata2'
2 |
3 | module Nata2::Mysqldumpslow
4 | def self.dump(slow_queries, sort_order = 'c')
5 | summation = {}
6 | slow_queries.each do |slow_query|
7 | sql = slow_query[:sql]
8 | next unless sql
9 | normarized_sql = normalize(sql)
10 | summation = sum(summation, normarized_sql, slow_query)
11 | end
12 |
13 | summarized = summarize(summation)
14 | sort_summarized(summarized, sort_order)
15 | end
16 |
17 | private
18 |
19 | def self.normalize(sql)
20 | sql = sql.gsub(/\b\d+\b/, 'N')
21 | sql = sql.gsub(/\b0x[0-9A-Fa-f]+\b/, 'N')
22 | sql = sql.gsub(/''/, %q{'S'})
23 | sql = sql.gsub(/''/, %q{'S'})
24 | sql = sql.gsub(/(\\')/, '')
25 | sql = sql.gsub(/(\\')/, '')
26 | sql = sql.gsub(/'[^']+'/, %q{'S'})
27 | sql = sql.gsub(/'[^']+'/, %q{'S'})
28 | # abbreviate massive "in (...)" statements and similar
29 | # s!(([NS],){100,})!sprintf("$2,{repeated %d times}",length($1)/2)!eg;
30 | sql
31 | end
32 |
33 | def self.sum(summation, normarized_sql, slow_query)
34 | summation[normarized_sql] ||= {
35 | count: 0, user: [slow_query[:user]], host: [slow_query[:host]],
36 | query_time: 0.0, lock_time: 0.0,
37 | rows_sent: 0, rows_examined: 0,
38 | raw_sql: slow_query[:sql]
39 | }
40 |
41 | summation[normarized_sql][:count] += 1
42 | summation[normarized_sql][:user] << slow_query[:user]
43 | summation[normarized_sql][:host] << slow_query[:host]
44 | summation[normarized_sql][:query_time] += slow_query[:query_time]
45 | summation[normarized_sql][:lock_time] += slow_query[:lock_time]
46 | summation[normarized_sql][:rows_sent] += slow_query[:rows_sent]
47 | summation[normarized_sql][:rows_examined] += slow_query[:rows_examined]
48 |
49 | summation
50 | end
51 |
52 | def self.summarize(summation)
53 | summation.map do |normarized_sql, c|
54 | count = c[:count].to_f
55 | {
56 | count: count.to_i, user: c[:user].uniq, host: c[:host].uniq,
57 | average: {
58 | query_time: c[:query_time]/count, lock_time: c[:lock_time]/count,
59 | rows_sent: c[:rows_sent]/count, rows_examined: c[:rows_examined]/count
60 | },
61 | summation: {
62 | query_time: c[:query_time], lock_time: c[:lock_time],
63 | rows_sent: c[:rows_sent], rows_examined: c[:rows_examined]
64 | },
65 | normarized_sql: normarized_sql,
66 | raw_sql: c[:row_sql]
67 | }
68 | end
69 | end
70 |
71 | def self.sort_summarized(summarized, order)
72 | result = case order
73 | when 'at'
74 | summarized.sort_by { |query| query[:average][:query_time] }
75 | when 'al'
76 | summarized.sort_by { |query| query[:average][:lock_time] }
77 | when 'ar'
78 | summarized.sort_by { |query| query[:average][:rows_sent] }
79 | when 'c'
80 | summarized.sort_by { |query| query[:count] }
81 | when 't'
82 | summarized.sort_by { |query| query[:summation][:query_time] }
83 | when 'l'
84 | summarized.sort_by { |query| query[:summation][:lock_time] }
85 | when 'r'
86 | summarized.sort_by { |query| query[:summation][:rows_sent] }
87 | else
88 | raise ArgumentError, %q{sort order is either of 'at', 'al', 'ar', 't', 'l', 'r' or 'c'.}
89 | end
90 |
91 | result.reverse
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Nata2 [](https://travis-ci.org/studio3104/nata2)
2 |
3 | Nata2 is a tool that can be summarized by integrating the slow query log.
4 | require **Ruby 2.0 or later**.
5 |
6 | ## Usage
7 |
8 | #### Install
9 |
10 | git clone,
11 |
12 | ```
13 | $ git clone https://github.com/studio3104/nata2.git
14 | ```
15 |
16 | and bundle install.
17 |
18 | ```
19 | $ cd nata2
20 | $ bundle install
21 | ```
22 |
23 | #### Configurations
24 |
25 | describe the setting in `config.toml`.
26 | specify `dburl` of [Sequel](http://sequel.jeremyevans.net/).
27 |
28 | ```
29 | dburl = "mysql2://YOUR_SPECIFIED_USERNAME:YOUR_SPECIFIED_USER's_PASSWORD@YOUR_MySQL_HOST/nata2"
30 | ```
31 |
32 | **strongly recommend using `mysql2`.**
33 | because do not have enough test in other databases.
34 |
35 | #### Initialize database
36 |
37 | create `nata2` database.
38 |
39 | ```
40 | $ mysql -uroot -p -e'CREATE DATABASE `nata2`'
41 | ```
42 |
43 | create schema with [Ridgepole](https://github.com/winebarrel/ridgepole).
44 |
45 | ```
46 | $ bundle exec ridgepole -c '{ adapter: mysql2, database: nata2, username: YOUR_SPECIFIED_USERNAME, password: YOUR_SPECIFIED_USER's_PASSWORD, host: YOUR_MySQL_HOST }' --apply
47 | ```
48 |
49 | #### Launch
50 |
51 | ```
52 | $ bundle exec rackup
53 | ```
54 |
55 | ## Register a slow query
56 |
57 | Post parsed slow query log.
58 |
59 | ```
60 | http://nata2.server/api/1/:service_name/:host_name/:database_name
61 |
62 | {
63 | datetime: 1390883951,
64 | user: 'user',
65 | host: 'localhost',
66 | query_time: 2.001227,
67 | lock_time: 0.0,
68 | rows_sent: 1,
69 | rows_examined:0,
70 | sql: 'SELECT SLEEP(2)'
71 | }
72 | ```
73 |
74 | If using curl, you should create post request like this.
75 | Header needs `Content-Type: application/x-www-form-urlencoded` (-d use this Content-Type)
76 | Form Fotmat is `key1=value1&key2=value2...`
77 |
78 | ```
79 | curl -d "datetime=1390883951" \
80 | -d "user=aa" \
81 | -d "host=localhost" \
82 | -d "query_time=2.001227" \
83 | -d "lock_time=0" \
84 | -d "rows_sent=1" \
85 | -d "rows_examined=0" \
86 | -d "sql=SELECTSLEEP(2)" \
87 | http://nata2.server/api/1/:service_name/:host_name/:database_name
88 | ```
89 |
90 | #### Clients
91 |
92 | - [nata2-client](https://github.com/studio3104/nata2-client)
93 | - [fluent-plugin-nata2](https://github.com/studio3104/fluent-plugin-nata2)
94 |
95 | ## Contributing
96 |
97 | 1. Fork it ( http://github.com/studio3104/nata2/fork )
98 | 2. Create your feature branch (`git checkout -b my-new-feature`)
99 | 3. Commit your changes (`git commit -am 'Add some feature'`)
100 | 4. Push to the branch (`git push origin my-new-feature`)
101 | 5. Create new Pull Request
102 |
103 | ## License
104 |
105 | Copyright (c) 2014 studio3104
106 |
107 | MIT License
108 |
109 | Permission is hereby granted, free of charge, to any person obtaining
110 | a copy of this software and associated documentation files (the
111 | "Software"), to deal in the Software without restriction, including
112 | without limitation the rights to use, copy, modify, merge, publish,
113 | distribute, sublicense, and/or sell copies of the Software, and to
114 | permit persons to whom the Software is furnished to do so, subject to
115 | the following conditions:
116 |
117 | The above copyright notice and this permission notice shall be
118 | included in all copies or substantial portions of the Software.
119 |
120 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
121 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
122 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
123 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
124 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
125 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
126 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
127 |
128 |
--------------------------------------------------------------------------------
/spec/nata2/server_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Nata Server Controller' do
4 | let(:service_name) { TestData::ServiceName }
5 | let(:host_name) { TestData::HostName }
6 | let(:database_name) { TestData::DatabaseName }
7 |
8 | describe 'APIs' do
9 | it 'API document' do
10 | get '/docs/api'
11 | expect(last_response).to be_ok
12 | end
13 |
14 | describe 'POST /api/1/:service_name/:host_name/:database_name' do
15 | let(:post_data) { TestData::ParsedSlowQuery }
16 |
17 | it 'create a new slow query record' do
18 | post %Q{/api/1/#{service_name}/#{host_name}/#{database_name}}, post_data
19 | expect(last_response).to be_ok
20 | expect(JSON.parse(last_response.body)['error']).to eq(0)
21 | end
22 |
23 | it 'create a new slow query record without required params' do
24 | post_data_without_required_params = post_data.clone
25 | post_data_without_required_params.delete(:sql)
26 | post %Q{/api/1/#{service_name}/#{host_name}/#{database_name}}, post_data_without_required_params
27 | expect(last_response.status).to eq(400)
28 | expect(JSON.parse(last_response.body, symbolize_names: true)).to eq(error: 1, messages: { sql: 'sql: missing or blank' })
29 | end
30 | end
31 | end
32 |
33 | it 'top page' do
34 | get '/'
35 | expect(last_response).to be_ok
36 | end
37 |
38 | context 'a slow query view' do
39 | it '200' do
40 | get '/slow_query/1'
41 | expect(last_response).to be_ok
42 | end
43 | it '404' do
44 | get '/slow_query/18446744073709551616'
45 | expect(last_response).to be_not_found
46 | end
47 | end
48 |
49 | context 'a dumped slow query view' do
50 | it '200' do
51 | get '/dumped_query/eyJjb3VudCI6MiwidXNlciI6WyJ1c2VyIl0sImhvc3QiOlsibG9jYWxob3N0Il0sImF2ZXJhZ2UiOnsicXVlcnlfdGltZSI6Mi4wMDEyMjcsImxvY2tfdGltZSI6MC4wLCJyb3dzX3NlbnQiOjEuMCwicm93c19leGFtaW5lZCI6MC4wfSwic3VtbWF0aW9uIjp7InF1ZXJ5X3RpbWUiOjQuMDAyNDU0LCJsb2NrX3RpbWUiOjAuMCwicm93c19zZW50IjoyLCJyb3dzX2V4YW1pbmVkIjowfSwibm9ybWFyaXplZF9zcWwiOiJzZWxlY3Qgc2xlZXAoTikiLCJyYXdfc3FsIjpudWxsfQ=='
52 | expect(last_response).to be_ok
53 | end
54 | it '404' do
55 | get '/dumped_query/not_found'
56 | expect(last_response).to be_not_found
57 | end
58 | end
59 |
60 | context 'view per database' do
61 | it '200' do
62 | get %Q{/view/#{service_name}/#{host_name}/#{database_name}}
63 | expect(last_response).to be_ok
64 | end
65 | it '404' do
66 | get %Q{/view/NOT_REGISTERED_SERVICE/NOT_REGISTERED_HOST/NOT_REGISTERED_DATABASE}
67 | expect(last_response).to be_not_found
68 | end
69 | end
70 |
71 | context 'complex view per database' do
72 | it '200' do
73 | get %Q{/view_complex/#{service_name}/#{database_name}}
74 | expect(last_response).to be_ok
75 | end
76 | it '404' do
77 | get %Q{/view_complex/NOT_REGISTERED_SERVICE/NOT_REGISTERED_DATABASE}
78 | expect(last_response).to be_not_found
79 | end
80 | end
81 |
82 | context 'dump view per database' do
83 | it '200' do
84 | get %Q{/dump/#{service_name}/#{host_name}/#{database_name}}
85 | expect(last_response).to be_ok
86 | end
87 | it '404' do
88 | get %Q{/dump/NOT_REGISTERED_SERVICE/NOT_REGISTERED_HOST/NOT_REGISTERED_DATABASE}
89 | expect(last_response).to be_not_found
90 | end
91 | end
92 |
93 | context 'complex dump view per database' do
94 | it '200' do
95 | get %Q{/dump_complex/#{service_name}/#{database_name}}
96 | expect(last_response).to be_ok
97 | end
98 | it '404' do
99 | get %Q{/dump_complex/NOT_REGISTERED_SERVICE/NOT_REGISTERED_DATABASE}
100 | expect(last_response).to be_not_found
101 | end
102 | end
103 |
104 | context 'list view per database' do
105 | it '200' do
106 | get %Q{/list/#{service_name}/#{host_name}/#{database_name}}
107 | expect(last_response).to be_ok
108 | end
109 | it '404' do
110 | get %Q{/list/NOT_REGISTERED_SERVICE/NOT_REGISTERED_HOST/NOT_REGISTERED_DATABASE}
111 | expect(last_response).to be_not_found
112 | end
113 | end
114 |
115 | context 'complex list view per database' do
116 | it '200' do
117 | get %Q{/list_complex/#{service_name}/#{database_name}}
118 | expect(last_response).to be_ok
119 | end
120 | it '404' do
121 | get %Q{/list_complex/NOT_REGISTERED_SERVICE/NOT_REGISTERED_DATABASE}
122 | expect(last_response).to be_not_found
123 | end
124 | end
125 | end
126 |
--------------------------------------------------------------------------------
/views/view.slim:
--------------------------------------------------------------------------------
1 | h3
2 | span #{@service_name}
3 | - if @host_name
4 | small
5 | span
6 | span.glyphicon.glyphicon-hand-right
7 | span
8 | span #{@host_name}
9 | - if @database_name
10 | small
11 | span
12 | span.glyphicon.glyphicon-hand-right
13 | span
14 | span #{@database_name}
15 |
16 | span#periodSwitch style="float: right;"
17 | .btn-group.btn-group-xs
18 | - if @time_range == 'd'
19 | button.btn.btn-primary.active disabled="" type="button" 24時間
20 | - else
21 | a.btn.btn-default disabled="" href="?t=d" 24時間
22 | - if @time_range == 'w'
23 | button.btn.btn-primary.active disabled="" type="button" 1週間
24 | - else
25 | a.btn.btn-default disabled="" href="?t=w" 1週間
26 | - if @time_range == 'm'
27 | button.btn.btn-primary.active disabled="" type="button" 1ヶ月
28 | - else
29 | a.btn.btn-default disabled="" href="?t=m" 1ヶ月
30 | - if @time_range == 'y'
31 | button.btn.btn-primary.active disabled="" type="button" 1年
32 | - else
33 | a.btn.btn-default disabled="" href="?t=y" 1年
34 |
35 | - list_path = @path.sub(/^\/view/, '/list')
36 | - dump_path = @path.sub(/^\/view/, '/dump')
37 | div#switch align="right"
38 | .btn-group.btn-group-xs
39 | button.btn.btn-default#switchlist disabled="" type="button" onclick="loadSlowQueries('list', '#{list_path}', '?#{URI.encode_www_form(@params.reject { |k, _| %w[ sort ].include?(k) })}')"
40 | span.glyphicon.glyphicon-list
41 | b 履歴
42 | .btn-group.btn-group-xs
43 | - button_params = @params.reject { |k, _| %w[ page sort ].include?(k) }
44 | button.btn.btn-default#switchdumpc disabled="" type="button" onclick="loadSlowQueries('dumpc', '#{dump_path}', '?#{URI.encode_www_form(button_params.merge(sort: 'c'))}')"
45 | span.glyphicon.glyphicon-plus
46 | b 合計回数順
47 | button.btn.btn-default#switchdumpt disabled="" type="button" onclick="loadSlowQueries('dumpt', '#{dump_path}', '?#{URI.encode_www_form(button_params.merge(sort: 't'))}')"
48 | span.glyphicon.glyphicon-time
49 | b 合計クエリ実行時間順
50 | button.btn.btn-default#switchdumpl disabled="" type="button" onclick="loadSlowQueries('dumpl', '#{dump_path}', '?#{URI.encode_www_form(button_params.merge(sort: 'l'))}')"
51 | span.glyphicon.glyphicon-lock
52 | b 合計ロック時間順
53 | button.btn.btn-default#switchdumpr disabled="" type="button" onclick="loadSlowQueries('dumpr', '#{dump_path}', '?#{URI.encode_www_form(button_params.merge(sort: 'r'))}')"
54 | span.glyphicon.glyphicon-send
55 | b 合計フェッチ行数順
56 | .btn-group.btn-group-xs
57 | button.btn.btn-default#switchdumpat disabled="" type="button" onclick="loadSlowQueries('dumpat', '#{dump_path}', '?#{URI.encode_www_form(button_params.merge(sort: 'at'))}')"
58 | span.glyphicon.glyphicon-time
59 | b 平均クエリ実行時間順
60 | button.btn.btn-default#switchdumpal disabled="" type="button" onclick="loadSlowQueries('dumpal', '#{dump_path}', '?#{URI.encode_www_form(button_params.merge(sort: 'al'))}')"
61 | span.glyphicon.glyphicon-lock
62 | b 平均ロック時間順
63 | button.btn.btn-default#switchdumpar disabled="" type="button" onclick="loadSlowQueries('dumpar', '#{dump_path}', '?#{URI.encode_www_form(button_params.merge(sort: 'ar'))}')"
64 | span.glyphicon.glyphicon-send
65 | b 平均フェッチ行数順
66 |
67 | h6
68 | - @labels.each do |name, meta|
69 | a href="#{meta[:path]}"
70 | span.label style="background-color : #{meta[:color]}" #{name}
71 |
72 | div id="count_slow_queries" style="height: 200px;"
73 |
74 | #slowQueries
75 |
76 | javascript:
77 | function loadSlowQueries(id_num, url, params) {
78 | getSlowQueries(id_num, url, params);
79 | window.history.pushState('', '', params);
80 | }
81 |
82 | function getSlowQueries(id, url, params) {
83 | $.ajax({
84 | beforeSend: function(){
85 | $("#switch button").attr("disabled","true");
86 | $("#periodSwitch button").attr("disabled","true");
87 | $("#periodSwitch a").attr("disabled","true");
88 | },
89 | url: url + params,
90 | success: function(result) {
91 | $("#slowQueries").html(result);
92 | },
93 | complete: function(xhr, event) {
94 | $("#switch button").removeAttr("disabled");
95 | $("#periodSwitch button").removeAttr("disabled");
96 | $("#periodSwitch a").removeAttr("disabled");
97 | $("#switch button").addClass("btn-default")
98 | $("#switch button").removeClass("btn-primary active")
99 | $("#switch" + id).addClass("btn-primary active");
100 | $("#switch" + id).removeClass("btn-default");
101 | }
102 | });
103 | }
104 |
105 | function drawGraph(id, data, labels, colors) {
106 | new Morris.Area({
107 | element: id,
108 | data: data,
109 | xkey: 'period',
110 | ykeys: labels,
111 | labels: labels,
112 | lineColors: colors,
113 | pointSize: 0,
114 | hideHover: true
115 | });
116 | }
117 |
118 | window.onload = function() {
119 | loadSlowQueries('#{@root}#{@params['sort']}', '#{@path.sub(/^\/view/, '/' + @root)}', '?#{URI.encode_www_form(@params)}');
120 | graph_data = eval('#{{JSON.generate(@graph_data)}}');
121 | console.log(graph_data);
122 | if (graph_data.length === 0) {
123 | $('#count_slow_queries').html("NO DATA");
124 | } else {
125 | drawGraph('count_slow_queries', graph_data, eval('#{{JSON.generate(@labels.keys)}}'), eval('#{{JSON.generate(@labels.values.map { |l| l[:color] })}}'));
126 | };
127 | }
128 |
129 | function getUrlVars(hoge) {
130 | var vars = {}, hash;
131 | var hashes = hoge.slice(hoge.indexOf('?') + 1).split('&');
132 | for (var i = 0; i < hashes.length; i++) {
133 | hash = hashes[i].split('=');
134 | vars[hash[0]] = hash[1];
135 | }
136 | return vars;
137 | }
138 |
139 | window.onpopstate = function(event) {
140 | var id;
141 | var path;
142 | var params = getUrlVars(location.search);
143 | if (typeof params.sort === 'undefined') {
144 | id = 'list';
145 | path = location.pathname.replace(/^\/view/, '/list');
146 | } else {
147 | id = 'dump' + params['sort'];
148 | path = location.pathname.replace(/^\/view/, '/dump');
149 | };
150 | getSlowQueries(id, path, location.search);
151 | };
152 |
--------------------------------------------------------------------------------
/lib/nata2/data.rb:
--------------------------------------------------------------------------------
1 | require 'nata2'
2 | require 'nata2/config'
3 | require 'nata2/mysqldumpslow'
4 | require 'time'
5 | require 'sequel'
6 |
7 | #DB = Sequel.connect(Nata2::Config.get(:dburl), logger: Nata2.logger)
8 | DB = Sequel.connect(Nata2::Config.get(:dburl))#, logger: Nata2.logger)
9 |
10 | class Nata2::Data
11 | def initialize
12 | @bundles ||= DB.from(:bundles)
13 | @slow_queries ||= DB.from(:slow_queries)
14 | @explains ||= DB.from(:explains)
15 | end
16 |
17 | def find_bundles(service_name: nil, host_name: nil, database_name: nil)
18 | bundles_where = { service_name: service_name, host_name: host_name, database_name: database_name }
19 | bundles_where.delete_if { |k,v| v.nil? }
20 | @bundles.where(bundles_where).order(:service_name, :database_name, :host_name).all
21 | end
22 |
23 | def get_slow_queries(
24 | id: nil, sort_by_date: false, limit: nil, offset: nil,
25 | from_datetime: 0, to_datetime: Time.now.to_i,
26 | service_name: nil, host_name: nil, database_name: nil
27 | )
28 | bundles_where = { service_name: service_name, host_name: host_name, database_name: database_name }
29 | bundles_where.delete_if { |k,v| v.nil? }
30 | slow_queries_where = id ? { slow_queries__id: id } : {}
31 |
32 | result = if sort_by_date
33 | @bundles.where(bundles_where).left_outer_join(
34 | :slow_queries, bundle_id: :id
35 | ).where(
36 | slow_queries_where
37 | ).where {
38 | (datetime >= from_datetime) & (datetime <= to_datetime)
39 | }.reverse_order(
40 | :datetime
41 | ).limit(limit).offset(offset)
42 | else
43 | @bundles.where(bundles_where).left_outer_join(
44 | :slow_queries, bundle_id: :id
45 | ).where(
46 | slow_queries_where
47 | ).where {
48 | (datetime >= from_datetime) & (datetime <= to_datetime)
49 | }.limit(limit).offset(offset)
50 | end
51 | result.all
52 | end
53 |
54 | # for graph data
55 | def get_slow_queries_count_by_period(
56 | per_day: false,
57 | id: nil, sort_by_date: false, limit: nil, offset: nil,
58 | from_datetime: 0, to_datetime: Time.now.to_i,
59 | service_name: nil, host_name: nil, database_name: nil
60 | )
61 | bundles_where = { service_name: service_name, host_name: host_name, database_name: database_name }
62 | bundles_where.delete_if { |k,v| v.nil? }
63 | slow_queries_where = id ? { slow_queries__id: id } : {}
64 |
65 | period = per_day ? :slow_queries__period_per_day : :slow_queries__period_per_hour
66 | @bundles.where(bundles_where).left_outer_join(
67 | :slow_queries, bundle_id: :id
68 | ).where(
69 | slow_queries_where
70 | ).where {
71 | (datetime >= from_datetime) & (datetime <= to_datetime)
72 | }.select_group(
73 | :bundles__service_name,
74 | :bundles__host_name,
75 | :bundles__database_name,
76 | period
77 | ).select_append {
78 | count(period).as(:count)
79 | }.reverse_order(
80 | :datetime
81 | ).limit(limit).offset(offset)
82 | end
83 |
84 | def get_summarized_slow_queries(sort_order, slow_queries)
85 | Nata2::Mysqldumpslow.dump(slow_queries, sort_order)
86 | end
87 |
88 | def get_explains(type: nil)
89 | end
90 |
91 | def register_slow_query(service_name, host_name, database_name, slow_query)
92 | bundles = find_or_create_bundles(service_name, host_name, database_name)
93 | result = nil
94 | current_time = Time.now.to_i
95 | sql = slow_query[:sql] #!!validation!!
96 |
97 | DB.transaction do
98 | @slow_queries.insert(
99 | bundle_id: bundles[:id],
100 | datetime: slow_query[:datetime],
101 | period_per_hour: Time.parse(Time.at(slow_query[:datetime]).to_s.sub(/:\d\d:\d\d/,':00:00')).to_i, # 2015-05-08 19:33:38 +0900 -> 2015-05-08 19:00:00 +0900
102 | period_per_day: Time.parse(Time.at(slow_query[:datetime]).to_s.sub(/\d\d:\d\d:\d\d/,'00:00:00')).to_i, # 2015-05-08 19:33:38 +0900 -> 2015-05-08 00:00:00 +0900
103 | user: slow_query[:user], host: slow_query[:host],
104 | query_time: slow_query[:query_time], lock_time: slow_query[:lock_time],
105 | rows_sent: slow_query[:rows_sent], rows_examined: slow_query[:rows_examined],
106 | sql: sql,
107 | created_at: current_time, updated_at: current_time
108 | )
109 |
110 | #!!depending on the transaction isolation. verification required.!!
111 | result = @slow_queries.select(:id).where(bundle_id: bundles[:id]).reverse_order(:id).limit(1).first
112 | end
113 |
114 | result
115 | end
116 |
117 | def register_explain(slow_query_id, explain)
118 | result = nil
119 | current_time = Time.now.to_i
120 |
121 | DB.transaction do
122 | @explains.where(slow_query_id: slow_query_id).delete
123 | explain.each do |e|
124 | @explains.insert(
125 | slow_query_id: slow_query_id,
126 | explain_id: e[:id], select_type: e[:select_type],
127 | table: e[:table], type: e[:type], possible_keys: e[:possible_keys],
128 | key: e[:key], key_len: e[:key_len], ref: e[:ref], rows: e[:rows], extra: e[:extra],
129 | created_at: current_time, updated_at: current_time
130 | )
131 | end
132 |
133 | @slow_queries.where(id: slow_query_id).update(explain: 'done')
134 | result = @explains.select(:id, :slow_query_id).where(slow_query_id: slow_query_id).all
135 | end
136 |
137 | result
138 | end
139 |
140 | def migrate_data_for_1_0_0
141 | DB.transaction do
142 | @slow_queries.each do |slow_query|
143 | period_per_hour = Time.parse(Time.at(slow_query[:datetime]).to_s.sub(/:\d\d:\d\d/,':00:00')).to_i # 2015-05-08 19:33:38 +0900 -> 2015-05-08 19:00:00 +0900
144 | period_per_day = Time.parse(Time.at(slow_query[:datetime]).to_s.sub(/\d\d:\d\d:\d\d/,'00:00:00')).to_i # 2015-05-08 19:33:38 +0900 -> 2015-05-08 00:00:00 +0900
145 | @slow_queries.where(id: slow_query[:id]).update(period_per_hour: period_per_hour, period_per_day: period_per_day)
146 | end
147 | end
148 | end
149 |
150 | private
151 |
152 | def config(name)
153 | Nata2::Config.get(name)
154 | end
155 |
156 | def find_or_create_bundles(service_name, host_name, database_name)
157 | bundles = find_bundles(service_name: service_name, host_name: host_name, database_name: database_name).first
158 | return bundles if bundles
159 |
160 | DB.transaction do
161 | current_time = Time.now.to_i
162 | color = '#' #create random color code
163 | 6.times { color = color + %w{0 1 2 3 4 5 6 7 8 9 a b c d e f}.shuffle.first }
164 |
165 | @bundles.insert(
166 | service_name: service_name,
167 | host_name: host_name,
168 | database_name: database_name,
169 | color: color,
170 | created_at: current_time,
171 | updated_at: current_time,
172 | )
173 |
174 | bundles = @bundles.where(service_name: service_name, host_name: host_name, database_name: database_name).first
175 | end
176 |
177 | bundles
178 | end
179 | end
180 |
--------------------------------------------------------------------------------
/lib/nata2/server.rb:
--------------------------------------------------------------------------------
1 | require 'nata2'
2 | require 'nata2/data'
3 | require 'nata2/helpers'
4 | require 'json'
5 | require 'base64'
6 | require 'uri'
7 | require 'sinatra/base'
8 | require 'sinatra/json'
9 | require 'slim'
10 |
11 | module Nata2
12 | class Server < Sinatra::Base
13 | configure do
14 | Slim::Engine.default_options[:pretty] = true
15 | app_root = File.dirname(__FILE__) + '/../..'
16 | set :public_folder, app_root + '/public'
17 | set :views, app_root + '/views'
18 | end
19 |
20 | configure :development do
21 | require 'sinatra/reloader'
22 | register Sinatra::Reloader
23 | set :show_exception, false
24 | set :show_exception, :after_handler
25 | end
26 |
27 | SUPPRESS_KEYS_TO_OPTIMIZE = %w[ service_name host_name database_name page amp splat captures ]
28 | helpers do
29 | def optimize_params(_params)
30 | params = _params.dup
31 | SUPPRESS_KEYS_TO_OPTIMIZE.each { |key| params.delete(key) }
32 | params
33 | end
34 |
35 | include Nata2::Helpers
36 | end
37 |
38 | not_found do
39 | '404'
40 | end
41 |
42 | get '/slow_query/:query_id' do
43 | @slow_query = data.get_slow_queries(id: params[:query_id]).first
44 | raise Sinatra::NotFound unless @slow_query
45 | slim :slow_query
46 | end
47 |
48 | get '/dumped_query/:dumped_query_base64encoded' do
49 | begin
50 | @dumped_query = JSON.parse(Base64.decode64(params[:dumped_query_base64encoded]), symbolize_names: true)
51 | rescue JSON::ParserError
52 | raise Sinatra::NotFound
53 | end
54 | slim :dumped_query
55 | end
56 |
57 | get '/view/:service_name/:host_name/:database_name' do
58 | @service_name = params['service_name']
59 | @host_name = params[:host_name]
60 | @database_name = params[:database_name]
61 | bundles = data.find_bundles(service_name: @service_name, host_name: @host_name, database_name: @database_name)
62 | raise Sinatra::NotFound if bundles.empty?
63 | @time_range = params['t'] || 'w'
64 | @graph_data = get_graph_data(@service_name, @host_name, @database_name, @time_range)
65 | @path = request.path
66 | @labels = labels(@service_name, @host_name, @database_name)
67 | @params = optimize_params(params)
68 | @root = @params.has_key?('sort') ? 'dump' : 'list'
69 | slim :view
70 | end
71 |
72 | get '/view_complex/:service_name/:database_name' do
73 | @service_name = params['service_name']
74 | @database_name = params[:database_name]
75 | bundles = data.find_bundles(service_name: @service_name, database_name: @database_name)
76 | raise Sinatra::NotFound if bundles.empty?
77 | @path = request.path
78 | @labels = labels(@service_name, @host_name, @database_name)
79 | @time_range = params['t'] || 'w'
80 | @graph_data = get_graph_data(@service_name, @host_name, @database_name, @time_range)
81 | @params = optimize_params(params)
82 | @root = @params.has_key?('sort') ? 'dump' : 'list'
83 | slim :view
84 | end
85 |
86 | get '/dump/:service_name/:host_name/:database_name' do
87 | sort = params['sort'] || 'c'
88 | service_name = params[:service_name]
89 | host_name = params[:host_name]
90 | database_name = params[:database_name]
91 | bundles = data.find_bundles(service_name: service_name, host_name: host_name, database_name: database_name)
92 | raise Sinatra::NotFound if bundles.empty?
93 | from = from_datetime(params['t'] || 'w')
94 | slow_queries = data.get_slow_queries(from_datetime: from, service_name: service_name, host_name: host_name, database_name: database_name)
95 | @slow_queries = data.get_summarized_slow_queries(sort, slow_queries)
96 | slim :dump
97 | end
98 |
99 | get '/dump_complex/:service_name/:database_name' do
100 | sort = params['sort'] || 'c'
101 | service_name = params[:service_name]
102 | database_name = params[:database_name]
103 | bundles = data.find_bundles(service_name: service_name, database_name: database_name)
104 | raise Sinatra::NotFound if bundles.empty?
105 | from = from_datetime(params['t'] || 'w')
106 | slow_queries = data.get_slow_queries(from_datetime: from, service_name: service_name, database_name: database_name)
107 | @slow_queries = data.get_summarized_slow_queries(sort, slow_queries)
108 | slim :dump
109 | end
110 |
111 | get '/list/:service_name/:host_name/:database_name' do
112 | service_name = params[:service_name]
113 | host_name = params[:host_name]
114 | database_name = params[:database_name]
115 | bundles = data.find_bundles(service_name: service_name, host_name: host_name, database_name: database_name)
116 | raise Sinatra::NotFound if bundles.empty?
117 | from = from_datetime(params['t'] || 'w')
118 | @page = params[:page] ? params[:page].to_i : 1
119 | @params = optimize_params(params)
120 | limit = 101
121 | offset = limit * (@page - 1) - 1
122 | offset = offset < 0 ? 0 : offset
123 |
124 | slow_queries = data.get_slow_queries(sort_by_date: true, from_datetime: from, service_name: service_name, host_name: host_name, database_name: database_name, limit: limit, offset: offset)
125 | if slow_queries.size <= 100
126 | @disabled_next = true
127 | else
128 | slow_queries.pop
129 | end
130 | @slow_queries_per_day = {}
131 | slow_queries.each do |slow_query|
132 | day = Time.at(slow_query[:datetime]).strftime('%Y/%m/%d')
133 | @slow_queries_per_day[day] ||= []
134 | @slow_queries_per_day[day] << slow_query
135 | end
136 |
137 | slim :list
138 | end
139 |
140 | get '/list_complex/:service_name/:database_name' do
141 | service_name = params[:service_name]
142 | database_name = params[:database_name]
143 | bundles = data.find_bundles(service_name: service_name, database_name: database_name)
144 | raise Sinatra::NotFound if bundles.empty?
145 | from = from_datetime(params['t'] || 'w')
146 | @page = params[:page] ? params[:page].to_i : 1
147 | @params = optimize_params(params)
148 | limit = 101
149 | offset = limit * (@page - 1) - 1
150 | offset = offset < 0 ? 0 : offset
151 |
152 | slow_queries = data.get_slow_queries(sort_by_date: true, from_datetime: from, service_name: service_name, database_name: database_name, limit: limit, offset: offset)
153 | if slow_queries.size <= 100
154 | @disabled_next = true
155 | else
156 | slow_queries.pop
157 | end
158 | @slow_queries_per_day = {}
159 | slow_queries.each do |slow_query|
160 | day = Time.at(slow_query[:datetime]).strftime('%Y/%m/%d')
161 | @slow_queries_per_day[day] ||= []
162 | @slow_queries_per_day[day] << slow_query
163 | end
164 |
165 | slim :list
166 | end
167 |
168 | get '/' do
169 | @bundles = {}
170 | @complex = {}
171 | complex = {}
172 | data.find_bundles.each do |bundle|
173 | service, database = [ bundle[:service_name], bundle[:database_name] ]
174 | @bundles[service] ||= []
175 | @bundles[service] << { color: bundle[:color], database: database, host: bundle[:host_name] }
176 | @complex[service] ||= []
177 | next if @complex[service].include?(database)
178 | complex[service] ||= {}
179 | complex[service][database] ||= 0
180 | complex[service][database] += 1
181 | @complex[service] << database if complex[service][database] > 1
182 | end
183 | slim :index
184 | end
185 |
186 | post '/api/1/:service_name/:host_name/:database_name' do
187 | req_params = validate(params, {
188 | service_name: { rule: rule(:not_blank) }, host_name: { rule: rule(:not_blank) }, database_name: { rule: rule(:not_blank) },
189 | user: { rule: rule(:regexp, /.*/) }, host: { rule: rule(:regexp, /.*/) },
190 | query_time: { rule: rule(:float) }, lock_time: { rule: rule(:float) },
191 | rows_sent: { rule: rule(:uint) }, rows_examined: { rule: rule(:uint) },
192 | sql: { rule: rule(:not_blank) }, datetime: { rule: rule(:natural) }
193 | })
194 |
195 | if req_params.has_error?
196 | halt 400, JSON.generate(error: 1, messages: req_params.errors)
197 | end
198 |
199 | req_params = req_params.hash
200 | result = data.register_slow_query(
201 | req_params.delete(:service_name),
202 | req_params.delete(:host_name),
203 | req_params.delete(:database_name),
204 | req_params
205 | )
206 |
207 | result ? JSON.generate(error: 0, data: result) : JSON.generate(error: 1, messages: [])
208 | end
209 |
210 | post '/api/1/explain/:slow_query_id' do
211 | slow_query_id = validate(params, { slow_query_id: { rule: rule(:not_blank) } })
212 | if slow_query_id.has_error?
213 | halt 400, json({ error: 1, messages: slow_query_id.errors })
214 | end
215 | slow_query_id = slow_query_id[:slow_query_id]
216 |
217 | post_spec = {
218 | id: { rule: rule(:natural) },
219 | select_type: { rule: rule(:choice,
220 | 'SIMPLE', 'PRIMARY',
221 | 'UNION', 'UNION RESULT', 'DEPENDENT UNION', 'UNCACHEABLE UNION', # UNION
222 | 'SUBQUERY', 'DEPENDENT SUBQUERY', 'UNCACHEABLE SUBQUERY', 'DERIVED' # SUBQUERY
223 | ) },
224 | table: { rule: rule(:not_blank) },
225 | type: { rule: rule(:choice, 'system' ,'const', 'eq_ref', 'ref', 'range', 'index', 'ALL') },
226 | possible_keys: { default: nil }, key: { default: nil }, key_len: { default: nil }, ref: { default: nil },
227 | rows: { rule: rule(:uint) }, extra: { default: nil },
228 | }
229 |
230 | explain = []
231 | explain_error = false
232 | params[:explain] = JSON.parse(request.body.read)
233 | if !params[:explain].is_a?(Array)
234 | halt 400, json({ error:1, messages: [] })
235 | end
236 | params[:explain].each do |p|
237 | record = p.symbolize_keys
238 | record = record.delete_if { |k,v| v == 'NULL' }
239 | exp = validate(record, post_spec)
240 | explain_error = true if exp.has_error?
241 | explain << exp
242 | end
243 |
244 | if explain_error
245 | halt 400, json({ error: 1, messages: explain.map { |exp| exp.errors } })
246 | end
247 |
248 | result = data.register_explain(slow_query_id, explain)
249 | result ? json({ error: 0, data: result }) : json({ error: 1, messages: [] })
250 | end
251 |
252 | get '/docs/api' do
253 | slim :'docs/api'
254 | end
255 | end
256 | end
257 |
--------------------------------------------------------------------------------
/public/css/bootstrap-theme.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.1.1 (http://getbootstrap.com)
3 | * Copyright 2011-2014 Twitter, Inc.
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */
6 |
7 | .btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn:active,.btn.active{background-image:none}.btn-default{background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;text-shadow:0 1px 0 #fff;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-color:#e8e8e8}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-color:#357ebd}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:linear-gradient(to bottom,#222 0,#282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0)}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)}
--------------------------------------------------------------------------------
/public/js/prettify.js:
--------------------------------------------------------------------------------
1 | !function(){var q=null;window.PR_SHOULD_USE_CONTINUATION=!0;
2 | (function(){function S(a){function d(e){var b=e.charCodeAt(0);if(b!==92)return b;var a=e.charAt(1);return(b=r[a])?b:"0"<=a&&a<="7"?parseInt(e.substring(1),8):a==="u"||a==="x"?parseInt(e.substring(2),16):e.charCodeAt(1)}function g(e){if(e<32)return(e<16?"\\x0":"\\x")+e.toString(16);e=String.fromCharCode(e);return e==="\\"||e==="-"||e==="]"||e==="^"?"\\"+e:e}function b(e){var b=e.substring(1,e.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),e=[],a=
3 | b[0]==="^",c=["["];a&&c.push("^");for(var a=a?1:0,f=b.length;a122||(l<65||h>90||e.push([Math.max(65,h)|32,Math.min(l,90)|32]),l<97||h>122||e.push([Math.max(97,h)&-33,Math.min(l,122)&-33]))}}e.sort(function(e,a){return e[0]-a[0]||a[1]-e[1]});b=[];f=[];for(a=0;ah[0]&&(h[1]+1>h[0]&&c.push("-"),c.push(g(h[1])));c.push("]");return c.join("")}function s(e){for(var a=e.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),c=a.length,d=[],f=0,h=0;f=2&&e==="["?a[f]=b(l):e!=="\\"&&(a[f]=l.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return a.join("")}for(var x=0,m=!1,j=!1,k=0,c=a.length;k=5&&"lang-"===w.substring(0,5))&&!(t&&typeof t[1]==="string"))f=!1,w="src";f||(r[z]=w)}h=c;c+=z.length;if(f){f=t[1];var l=z.indexOf(f),B=l+f.length;t[2]&&(B=z.length-t[2].length,l=B-f.length);w=w.substring(5);H(j+h,z.substring(0,l),g,k);H(j+h+l,f,I(w,f),k);H(j+h+B,z.substring(B),g,k)}else k.push(j+h,w)}a.g=k}var b={},s;(function(){for(var g=a.concat(d),j=[],k={},c=0,i=g.length;c=0;)b[n.charAt(e)]=r;r=r[1];n=""+r;k.hasOwnProperty(n)||(j.push(r),k[n]=q)}j.push(/[\S\s]/);s=S(j)})();var x=d.length;return g}function v(a){var d=[],g=[];a.tripleQuotedStrings?d.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?d.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/,
10 | q,"'\"`"]):d.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&g.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var b=a.hashComments;b&&(a.cStyleComments?(b>1?d.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):d.push(["com",/^#(?:(?:define|e(?:l|nd)if|else|error|ifn?def|include|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),g.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h(?:h|pp|\+\+)?|[a-z]\w*)>/,q])):d.push(["com",
11 | /^#[^\n\r]*/,q,"#"]));a.cStyleComments&&(g.push(["com",/^\/\/[^\n\r]*/,q]),g.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));if(b=a.regexLiterals){var s=(b=b>1?"":"\n\r")?".":"[\\S\\s]";g.push(["lang-regex",RegExp("^(?:^^\\.?|[+-]|[!=]=?=?|\\#|%=?|&&?=?|\\(|\\*=?|[+\\-]=|->|\\/=?|::?|<=?|>>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*("+("/(?=[^/*"+b+"])(?:[^/\\x5B\\x5C"+b+"]|\\x5C"+s+"|\\x5B(?:[^\\x5C\\x5D"+b+"]|\\x5C"+
12 | s+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&g.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&g.push(["kwd",RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),q]);d.push(["pln",/^\s+/,q," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");g.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,
13 | q],["pun",RegExp(b),q]);return C(d,g)}function J(a,d,g){function b(a){var c=a.nodeType;if(c==1&&!x.test(a.className))if("br"===a.nodeName)s(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((c==3||c==4)&&g){var d=a.nodeValue,i=d.match(m);if(i)c=d.substring(0,i.index),a.nodeValue=c,(d=d.substring(i.index+i[0].length))&&a.parentNode.insertBefore(j.createTextNode(d),a.nextSibling),s(a),c||a.parentNode.removeChild(a)}}function s(a){function b(a,c){var d=
14 | c?a.cloneNode(!1):a,e=a.parentNode;if(e){var e=b(e,1),g=a.nextSibling;e.appendChild(d);for(var i=g;i;i=g)g=i.nextSibling,e.appendChild(i)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),d;(d=a.parentNode)&&d.nodeType===1;)a=d;c.push(a)}for(var x=/(?:^|\s)nocode(?:\s|$)/,m=/\r\n?|\n/,j=a.ownerDocument,k=j.createElement("li");a.firstChild;)k.appendChild(a.firstChild);for(var c=[k],i=0;i=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*=l&&(b+=2);g>=B&&(r+=2)}}finally{if(f)f.style.display=h}}catch(u){D.console&&console.log(u&&u.stack||u)}}var D=window,y=["break,continue,do,else,for,if,return,while"],E=[[y,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],
18 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],M=[E,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],N=[E,"abstract,assert,boolean,byte,extends,final,finally,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"],
19 | O=[N,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,internal,into,is,let,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var,virtual,where"],E=[E,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],P=[y,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],
20 | Q=[y,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],W=[y,"as,assert,const,copy,drop,enum,extern,fail,false,fn,impl,let,log,loop,match,mod,move,mut,priv,pub,pure,ref,self,static,struct,true,trait,type,unsafe,use"],y=[y,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],R=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)\b/,
21 | V=/\S/,X=v({keywords:[M,O,E,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",P,Q,y],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),F={};p(X,["default-code"]);p(C([],[["pln",/^[^]+/],["dec",/^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",
22 | /^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^