ostrichpoll 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ *.iml
20
+ .idea
21
+
22
+ tumblrexamples
data/.rvmrc ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # This is an RVM Project .rvmrc file, used to automatically load the ruby
4
+ # development environment upon cd'ing into the directory
5
+
6
+ # First we specify our desired <ruby>[@<gemset>], the @gemset name is optional.
7
+ environment_id="ruby-1.9.2-p290"
8
+
9
+ #
10
+ # Uncomment the following lines if you want to verify rvm version per project
11
+ #
12
+ # rvmrc_rvm_version="1.10.1" # 1.10.1 seams as a safe start
13
+ # eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
14
+ # echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
15
+ # return 1
16
+ # }
17
+ #
18
+
19
+ #
20
+ # Uncomment following line if you want options to be set only for given project.
21
+ #
22
+ # PROJECT_JRUBY_OPTS=( --1.9 )
23
+ #
24
+ # The variable PROJECT_JRUBY_OPTS requires the following to be run in shell:
25
+ #
26
+ # chmod +x ${rvm_path}/hooks/after_use_jruby_opts
27
+ #
28
+
29
+ #
30
+ # First we attempt to load the desired environment directly from the environment
31
+ # file. This is very fast and efficient compared to running through the entire
32
+ # CLI and selector. If you want feedback on which environment was used then
33
+ # insert the word 'use' after --create as this triggers verbose mode.
34
+ #
35
+ if [[ -d "${rvm_path:-$HOME/.rvm}/environments" \
36
+ && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
37
+ then
38
+ \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
39
+
40
+ if [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]]
41
+ then
42
+ . "${rvm_path:-$HOME/.rvm}/hooks/after_use"
43
+ fi
44
+ else
45
+ # If the environment file has not yet been created, use the RVM CLI to select.
46
+ if ! rvm --create "$environment_id"
47
+ then
48
+ echo "Failed to create RVM environment '${environment_id}'."
49
+ return 1
50
+ fi
51
+ fi
52
+
53
+ #
54
+ # If you use an RVM gemset file to install a list of gems (*.gems), you can have
55
+ # it be automatically loaded. Uncomment the following and adjust the filename if
56
+ # necessary.
57
+ #
58
+ # filename=".gems"
59
+ # if [[ -s "$filename" ]]
60
+ # then
61
+ # rvm gemset import "$filename" | grep -v already | grep -v listed | grep -v complete | sed '/^$/d'
62
+ # fi
63
+
64
+ # If you use bundler, this might be useful to you:
65
+ # if [[ -s Gemfile ]] && ! command -v bundle >/dev/null
66
+ # then
67
+ # printf "%b" "The rubygem 'bundler' is not installed. Installing it now.\n"
68
+ # gem install bundler
69
+ # fi
70
+ # if [[ -s Gemfile ]] && command -v bundle
71
+ # then
72
+ # bundle install
73
+ # fi
74
+
75
+ if [[ $- == *i* ]] # check for interactive shells
76
+ then
77
+ echo "Using: $(tput setaf 2)$GEM_HOME$(tput sgr0)" # show the user the ruby and gemset they are using in green
78
+ else
79
+ echo "Using: $GEM_HOME" # don't use colors in interactive shells
80
+ fi
81
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ostrichpoll.gemspec
4
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,14 @@
1
+
2
+ Copyright 2012, Tumblr Inc.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # Ostrichpoll
2
+
3
+ Ostrichpoll is a tiny utility for monitoring Twitter Ostrich services.
4
+ Effectively it can monitor any service which exposes internal metrics in JSON
5
+ over HTTP.
6
+
7
+ Features:
8
+
9
+ * not a daemon: Ostrichpoll is intended to be run by `monit` or similar tool on a regular basis.
10
+ * validate multiple endpoints in a single execution
11
+ * specify normal ranges for metrics and gauges through YAML configuration
12
+ * support for "rate" measurements, specifying acceptable change per second for
13
+ a metric. (useful for counters and other monotonic metrics)
14
+
15
+
16
+ ## Installation
17
+
18
+ $ gem install ostrichpoll
19
+
20
+ ## Execution
21
+
22
+ Ostrichpoll may be executed with a simple `ostrichpoll` which will ensure a 200 response
23
+ is given from `127.0.0.1:9900/stats.json`.
24
+
25
+ You may change the endpoint:
26
+
27
+ ostrichpoll -u devbox:9999/stats.json
28
+
29
+ The interesting part of Ostrichpoll is when you give a configuration file
30
+ that specifies normal ranges for individual metrics:
31
+
32
+ ostrichpoll -c pollconfig.yml
33
+
34
+ Use `-d` to enable extra logging for debugging.
35
+
36
+ ## Configuration
37
+
38
+ Configuration is specified in the format:
39
+
40
+ ---
41
+ - url: 127.0.0.1:9900/stats.txt?period=10
42
+ rate_file: /tmp/parmesan-http-ostrich-rate.yml
43
+ validations:
44
+ counters/KafkaEventSink_messageDropped:
45
+ rate: true
46
+ normal_range: [0, 5]
47
+ missing: ignore
48
+ exit_code: 2
49
+ metrics/KafkaEventSink_append_msec/p99:
50
+ normal_range: [0, 10]
51
+ missing: error
52
+ exit_code: 3
53
+ - url: 127.0.0.1:9901/stats.txt
54
+ ...
55
+
56
+ ### Options
57
+
58
+ * `url` - an endpoint which exposes JSON stats. If this endpoint is not available, Ostrichpoll will exit with `-1` (really `255`).
59
+ * `rate_file` - where to store rate measurements between executions of Ostrichpoll.
60
+ * `validations` - a list of validations to execute with the following options:
61
+ * the key is the name of the metric. Nested metrics are supported through the `/` notation.
62
+ * `rate` when set to true, compute the change per second since this value was last seen. You must specify a `rate_file` for this to work. Obviously, on the first execution of Ostrichpoll this validation will be ignored.
63
+ * `normal_range` an array specifying the minimum and maximum (inclusive) values which are acceptable for this metric
64
+ * `missing` behavior if the metric is not seen in the output at all. (error by default)
65
+ * `exit_code` what value Ostrichpoll should exit with if this error is seen.
66
+
67
+ ### Notes
68
+ Ostrichpoll is setup to execute all validations on each execution, even if one of the early validations fails, the output from all validations is logged to stderr. However, the exit code is the exit code from the first erroring validation.
69
+
70
+
71
+ ## License
72
+
73
+ Copyright 2012, Tumblr Inc.
74
+
75
+ Licensed under the Apache License, Version 2.0 (the "License");
76
+ you may not use this file except in compliance with the License.
77
+ You may obtain a copy of the License at
78
+
79
+ http://www.apache.org/licenses/LICENSE-2.0
80
+
81
+ Unless required by applicable law or agreed to in writing, software
82
+ distributed under the License is distributed on an "AS IS" BASIS,
83
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
84
+ See the License for the specific language governing permissions and
85
+ limitations under the License.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bin/ostrichpoll ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'ostrichpoll'
File without changes
@@ -0,0 +1,74 @@
1
+ require 'ostrichpoll/ostrich_validator'
2
+
3
+ module OstrichPoll
4
+ # traverses a nested hashmap (the parsed config YAML)
5
+ # returning a set of hosts and validators
6
+ class ConfigParser
7
+ def self.parse(map)
8
+ hosts = []
9
+
10
+ map.each do |host_config|
11
+ host = Host.new
12
+ host_config.each do |key,value|
13
+ case key
14
+ when 'url'
15
+ host.url = value
16
+
17
+ when 'rate_file'
18
+ host.rate_file = value
19
+
20
+ when 'validations'
21
+ validations = parse_validations(value)
22
+
23
+ validations.each do |v|
24
+ v.host_instance = host
25
+ end
26
+
27
+ host.validations = validations
28
+
29
+ else
30
+ Log.warn "Unknown key: #{key}. Ignoring."
31
+ end
32
+ end
33
+ hosts << host
34
+ end
35
+
36
+ hosts
37
+ end
38
+
39
+ def self.parse_validations(validations)
40
+ return [] unless validations
41
+
42
+ validations.map do |name, v|
43
+ self.parse_validation name, v
44
+ end
45
+ end
46
+
47
+ def self.parse_validation(name, map)
48
+ validator = Validator.new
49
+ validator.metric = name
50
+ map.each do |key,value|
51
+ case key
52
+ when 'rate'
53
+ validator.rate = value
54
+
55
+ when 'normal_range'
56
+ # FIXME validate normal range (min <= max, etc.)
57
+ validator.normal_range = value
58
+
59
+ when 'missing'
60
+ validator.missing = value.to_sym
61
+
62
+ when 'exit_code'
63
+ validator.exit_code = value
64
+
65
+ else
66
+ Log.warn "Unknown key for validation: #{key}. Ignoring."
67
+ end
68
+ end
69
+
70
+ validator.verify!
71
+ validator
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,8 @@
1
+ module OstrichPoll
2
+ # generics
3
+ EXIT_OK = 0
4
+ EXIT_ERROR = -1
5
+
6
+ # could not connect to http endpoint
7
+ EXIT_NOHTTP = 1
8
+ end
@@ -0,0 +1,175 @@
1
+ #
2
+ # implements the actual validators
3
+ #
4
+
5
+ require 'json'
6
+ require 'yaml'
7
+ require 'net/http'
8
+
9
+ module OstrichPoll
10
+ class Host
11
+ attr_accessor :url
12
+ attr_accessor :rate_file
13
+ attr_accessor :validations
14
+
15
+
16
+ def initialize
17
+ validations = []
18
+ end
19
+
20
+ attr_accessor :stored_values
21
+ attr_accessor :stored_timestamp
22
+
23
+ def validate
24
+ uri = URI.parse url
25
+ response = Net::HTTP.get uri
26
+
27
+ # parse response
28
+ json = JSON.parse(response) rescue (
29
+ Log.error "Invalid JSON response: #{response}"
30
+ return EXIT_ERROR
31
+ )
32
+
33
+ @stored_values = {}
34
+ if rate_file
35
+ # read in rate-file
36
+ @stored_values = YAML.load_file(rate_file) rescue (
37
+ Log.warn "Could not parse rate file: #{rate_file}"
38
+ {}
39
+ )
40
+
41
+ @stored_timestamp = stored_values['ostrichpoll.timestamp']
42
+ unless @stored_timestamp
43
+ Log.warn "No 'ostrichpoll.timestamp' found in rate file: #{rate_file}"
44
+ end
45
+
46
+ # write out new rate file
47
+ json['ostrichpoll.timestamp'] = Time.now.to_i
48
+ File.open(rate_file, 'w') do |f|
49
+ f.puts json.to_yaml
50
+ end
51
+ end
52
+
53
+ # execute each validations:
54
+ retval = false
55
+ if validations
56
+ validations.each do |v|
57
+ value = v.check(find_value(json, v.metric))
58
+ retval = value unless retval
59
+ end
60
+ end
61
+
62
+ retval
63
+ end
64
+
65
+ def previous_reading(key)
66
+ return stored_timestamp, find_value(stored_values, key)
67
+ end
68
+
69
+ def find_value(map, key)
70
+ tree = map
71
+ key.split('/').each do |selector|
72
+ return nil unless tree.kind_of? Hash
73
+ tree = tree[selector]
74
+ end
75
+
76
+ tree
77
+ end
78
+ end
79
+
80
+ # this is a pretty weak and limiting definition of a validator,
81
+ # but it's quick to develop and clear how to extend
82
+ class Validator
83
+ attr_accessor :host_instance
84
+
85
+ attr_accessor :metric
86
+ attr_accessor :rate
87
+ attr_accessor :normal_range
88
+ attr_accessor :missing
89
+ attr_accessor :exit_code
90
+
91
+ def init
92
+ @rate = false
93
+ @exit_code = 1
94
+ @missing = :ignore
95
+ end
96
+
97
+ def verify!
98
+ Log.warn "Invalid metric #{metric.inspect}" unless metric.is_a? String
99
+ Log.warn "Invalid exit code: #{exit_code.inspect}" unless exit_code.is_a? Integer
100
+
101
+ if normal_range
102
+ Log.warn "Invalid normal range: #{normal_range.inspect}" unless normal_range.is_a? Array
103
+ end
104
+ end
105
+
106
+ def check (value)
107
+ Log.debug "#{host_instance.url} | Given: #{metric}=#{value}"
108
+
109
+ # error on missing value unless we ignore missing
110
+ unless value
111
+ unless missing == :ignore
112
+ Log.warn "#{metric}: value missing, treating as error; exit code #{exit_code}"
113
+ return exit_code
114
+ else
115
+ Log.debug "#{host_instance.url} | missing value, but set to ignore"
116
+ # not an error, but you can't check anything else
117
+ return false
118
+ end
119
+ end
120
+
121
+ # compute rate
122
+ if rate
123
+ timestamp, previous = host_instance.previous_reading(metric)
124
+
125
+ if previous
126
+ Log.debug "#{host_instance.url} | last seen: #{previous} @ #{timestamp}"
127
+
128
+ # change since last measure
129
+ value -= previous
130
+
131
+ # divide by seconds elapsed
132
+ value /= Time.now.to_i - timestamp
133
+
134
+ Log.debug "#{host_instance.url} | computed rate: #{value}"
135
+
136
+ else
137
+ # let it pass
138
+ Log.info "#{metric}: no previous reading for rate"
139
+ return false
140
+ end
141
+ end
142
+
143
+ # ensure value is within normal range
144
+ if normal_range
145
+ Log.debug "#{host_instance.url} | normal range: #{normal_range.inspect}"
146
+ case normal_range.size
147
+ when 1 # max
148
+ hi = normal_range.first
149
+
150
+ when 2 # min
151
+ lo = normal_range.first
152
+ hi = normal_range.last
153
+
154
+ else
155
+ # whatever, ignore
156
+ # the yaml deserializer shouldn't let this happen
157
+ end
158
+
159
+ if lo && value < lo
160
+ Log.warn "#{metric}: read value #{value} is below normal range minimum #{lo}; exit code #{exit_code}"
161
+ return exit_code
162
+ end
163
+
164
+ if hi && value > hi
165
+ Log.warn "#{metric}: read value #{value} is above normal range maximum #{hi}; exit code #{exit_code}"
166
+ return exit_code
167
+ end
168
+
169
+ Log.debug "#{host_instance.url} | within normal range"
170
+ end
171
+
172
+ false
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,6 @@
1
+ class String
2
+ def strip_heredoc
3
+ indent = scan(/^[ \t]*(?=\S)/).min.size || 0
4
+ gsub(/^[ \t]{#{indent}}/, '')
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module OstrichPoll
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,77 @@
1
+ require 'yaml'
2
+ require 'trollop'
3
+ require 'pp'
4
+ require 'net/http'
5
+ require 'logger'
6
+
7
+ require 'ostrichpoll/string'
8
+ require 'ostrichpoll/version'
9
+ require 'ostrichpoll/exit_codes'
10
+ require 'ostrichpoll/config_parser'
11
+
12
+ module OstrichPoll
13
+ Log = Logger.new(STDERR)
14
+
15
+ @opts = Trollop::options do
16
+ version = "ostrichpoll #{VERSION}"
17
+ banner_text = <<-EOS
18
+ A ruby utility for monitoring JSON endpoints (Twitter Ostrich, specifically)
19
+ for normal ranges of values.
20
+ EOS
21
+ banner(banner_text.strip_heredoc)
22
+
23
+ opt :configfile, "YAML configuration",
24
+ type: :string
25
+
26
+ opt :url, "url",
27
+ type: :string,
28
+ default: 'http://127.0.0.1:9900/stats.json'
29
+
30
+ opt :debug, "debug",
31
+ :default => false
32
+ end
33
+
34
+ if @opts[:configfile] and not File.readable_real?(@opts[:configfile])
35
+ Trollop::die "configuration file #{@opts[:configfile]} cannot be read"
36
+ end
37
+
38
+ if @opts[:debug]
39
+ Log.level = Logger::DEBUG
40
+ else
41
+ Log.level = Logger::WARN
42
+ end
43
+
44
+ # TODO rewrite the basic check in terms of a hard-coded validator, much cleaner
45
+ # check that the host+port respond to http
46
+ if @opts[:configfile]
47
+ yaml = YAML.load_file @opts[:configfile]
48
+ hosts = ConfigParser.parse(yaml)
49
+
50
+ retval = false
51
+ hosts.each do |h|
52
+ retval = h.validate unless retval
53
+ end
54
+
55
+ # use the exitcode, unless none is given
56
+ exit retval if retval
57
+
58
+ else
59
+ # if we don't have a config file, simply check that the host and port respond to http
60
+ begin
61
+ uri = URI.parse @opts[:url]
62
+ @response = Net::HTTP.get uri
63
+ rescue Exception => e
64
+ Log.warn e
65
+ exit EXIT_NOHTTP
66
+ end
67
+ end
68
+
69
+ rescue SystemExit => e
70
+ exit e.status
71
+
72
+ rescue Exception => e
73
+ Log.error e
74
+ exit EXIT_ERROR # exit with error
75
+ end
76
+
77
+ exit OstrichPoll::EXIT_OK
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/ostrichPoll/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Wiktor Macura"]
6
+ gem.email = ["wiktor@tumblr.com"]
7
+ gem.description = %q{Ostrichpoll is a tiny utility for monitoring Twitter Ostrich services. Effectively it can monitor any service which exposes internal metrics in JSON over HTTP.}
8
+ gem.summary = %q{Ostrichpoll is a tiny utility for monitoring Twitter Ostrich services.}
9
+ gem.homepage = "http://github.com/tumblr/ostrichpoll"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "ostrichpoll"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = OstrichPoll::VERSION
17
+
18
+ gem.add_development_dependency 'json'
19
+ gem.add_development_dependency 'trollop'
20
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ostrichpoll
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Wiktor Macura
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-08 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: &70093421674620 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70093421674620
25
+ - !ruby/object:Gem::Dependency
26
+ name: trollop
27
+ requirement: &70093413683700 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70093413683700
36
+ description: Ostrichpoll is a tiny utility for monitoring Twitter Ostrich services.
37
+ Effectively it can monitor any service which exposes internal metrics in JSON over
38
+ HTTP.
39
+ email:
40
+ - wiktor@tumblr.com
41
+ executables:
42
+ - ostrichpoll
43
+ extensions: []
44
+ extra_rdoc_files: []
45
+ files:
46
+ - .gitignore
47
+ - .rvmrc
48
+ - Gemfile
49
+ - LICENSE.md
50
+ - README.md
51
+ - Rakefile
52
+ - bin/ostrichpoll
53
+ - exampleconfigs/firehose.yml
54
+ - lib/ostrichpoll.rb
55
+ - lib/ostrichpoll/config_parser.rb
56
+ - lib/ostrichpoll/exit_codes.rb
57
+ - lib/ostrichpoll/ostrich_validator.rb
58
+ - lib/ostrichpoll/string.rb
59
+ - lib/ostrichpoll/version.rb
60
+ - ostrichPoll.gemspec
61
+ homepage: http://github.com/tumblr/ostrichpoll
62
+ licenses: []
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 1.8.10
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Ostrichpoll is a tiny utility for monitoring Twitter Ostrich services.
85
+ test_files: []