logplex-client 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.swp
2
+ .yardoc/
3
+ doc/
4
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ group :documentation do
6
+ gem "yard"
7
+ gem "yard-tomdoc"
8
+ end
9
+
10
+ group :testing do
11
+ gem "rspec"
12
+ gem "insist"
13
+ end
data/Makefile ADDED
@@ -0,0 +1,34 @@
1
+ GEMSPEC=$(shell ls *.gemspec | head -1)
2
+ VERSION=$(shell ruby -rubygems -e 'puts Gem::Specification.load("$(GEMSPEC)").version')
3
+ PROJECT=$(shell ruby -rubygems -e 'puts Gem::Specification.load("$(GEMSPEC)").name')
4
+ GEM=$(PROJECT)-$(VERSION).gem
5
+
6
+ .PHONY: test
7
+ test:
8
+ bundle exec rspec
9
+
10
+ .PHONY: package
11
+ package: $(GEM)
12
+
13
+ # Always build the gem
14
+ .PHONY: $(GEM)
15
+ $(GEM):
16
+ gem build $(PROJECT).gemspec
17
+
18
+ showdocs:
19
+ yard server --plugin yard-tomdoc -r
20
+
21
+ clean:
22
+ -rm -r .yardoc/ doc/ *.gem
23
+
24
+ .PHONY: install
25
+ install: $(GEM)
26
+ gem install $<
27
+
28
+ # Publish to gemgate
29
+ .PHONY: publish
30
+ publish: APPNAME=gemgate-heroku-internal-gems
31
+ publish: URL=https://$(APPNAME).herokuapp.com
32
+ publish: GEMGATE_AUTH=$(shell heroku config -a $(APPNAME) | awk '/GEMGATE_AUTH/ { print $$NF }')
33
+ publish: $(GEM)
34
+ curl -F file=@$(GEM) -u $(GEMGATE_AUTH) $(URL)
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ ## Logplex Client
2
+
3
+ ### command line interface
4
+
5
+ What follows are examples.
6
+
7
+ ```bash
8
+ # You must do this.
9
+ $ export LOGPLEX_URL=https://heroku:password@logplex.heroku.com
10
+
11
+ $ bin/logplex channels:create channel-blah
12
+ creating channel... done
13
+ channel id: 3858972
14
+
15
+ $ logplex channels:get 3858972
16
+ getting channel (3858972)... done
17
+
18
+ id: 3858972
19
+ no tokens
20
+ no drains
21
+
22
+ $ logplex channels:delete 3858972
23
+ deleting channel... done
24
+
25
+ $ logplex tokens:create 3858972 helloworld
26
+ creating token...
27
+ helloworld => t.7c55b81f-59b3-45bb-87b1-aefc0fd96a40
28
+
29
+ $ bin/logplex channels:get 3858972
30
+ getting channel (3858972)...done
31
+
32
+ id: 3858972
33
+ token: helloworld (t.7c55b81f-59b3-45bb-87b1-aefc0fd96a40)
34
+ no drains
35
+
36
+ $ bin/logplex channels:logs 3858972 --ps web.1
37
+ 2012-03-22T00:12:47+00:00 app[web.1]: foo
38
+ 2012-03-22T00:12:48+00:00 app[web.1]: bar
39
+ ```
40
+
41
+ ## Ruby Usage
42
+
43
+ ### ruby api/usage docs
44
+
45
+ You can view the rubydoc for this project by running:
46
+
47
+ ```bash
48
+ $ bundle install
49
+ $ make showdocs
50
+ ```
51
+
52
+ Then point your browser at <http://localhost:8808/> to view the HTML version of
53
+ the API docs.
54
+
55
+ ### Managing Logplex
56
+
57
+ ```ruby
58
+ # Get a new client.
59
+ => client = Logplex::Client.new("https://heroku:password@logplex.heroku.com")
60
+
61
+ # Create a named channel.
62
+ => channel = client.create_channel("channel-blah")
63
+
64
+ # Get a known channel by id
65
+ => channel = client.channel(1001)
66
+
67
+ # Destroy a channel
68
+ => channel.destroy
69
+
70
+ # Create a named token on a channel
71
+ => token = channel.create_token("app")
72
+
73
+ # Destroy a token
74
+ => token.destroy
75
+
76
+ # Create a drain
77
+ => drain = channel.create_drain
78
+
79
+ # Get a known drain by a drain id
80
+ => drain = channel.drain(12345)
81
+
82
+ # Make the drain point at a specific syslog receiver:
83
+ => drain.url = "syslog://example.com:1234/
84
+
85
+ # Destroy a drain
86
+ => drain.destroy
87
+ ```
88
+
89
+ ### Writing to Logplex
90
+
91
+ ```ruby
92
+ # Get an Emitter for writing logs using a token
93
+ => emitter = token.emitter
94
+
95
+ # Writing an event
96
+ # Note, you must be running on the heroku platform for this to work as
97
+ # currently the port used by log transport into logplex is firewalled.
98
+ => emitter.emit("processname", "message")
99
+ ```
100
+
101
+ ### Reading from Logplex
102
+
103
+ ```ruby
104
+ # Create a log session (for reading logs)
105
+ => session = channel.session(:num => 10)
106
+ # or tail things
107
+ => session = channel.session(:num => 10, :tail => true
108
+
109
+ # Read logs from a session
110
+ => session.each_event do |event|
111
+ puts event
112
+ end
113
+ ```
114
+
115
+ ### Logplex HTTP API
116
+
117
+ https://logplex.herokuapp.com/
data/bin/logplex ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "logplex/client"
4
+ require "logplex/client/cli"
5
+
6
+ begin
7
+ Logplex::Client::CLI.run
8
+ rescue Errno::EPIPE
9
+ #rescue Logplex::Client::Error => e
10
+ #$stderr.puts " ! #{e}"
11
+ #exit 1
12
+ end
@@ -0,0 +1,144 @@
1
+ require "logplex/namespace"
2
+ require "logplex/drain"
3
+ require "logplex/session"
4
+ require "logplex/token"
5
+
6
+ # Operations on a Logplex::Channel:
7
+ # Delete a channel - DELETE /v2/channels/<channel>
8
+ # API> channel.destroy => nil
9
+ # Get channel info - GET /v2/channels/<channel>
10
+ # API> channel.tokens => [Token, Token, ...]
11
+ # API> channel.drains => [Drain, Drain, ...]
12
+ # Get a token with known name: (just a wrapper of 'channel.tokens')
13
+ # API> channel.token(name) => Token
14
+ # Create a token - POST /v2/channels/<channel>/tokens { "name": "the token name" }
15
+ # API> channel.create_token(name) => Token
16
+ # Create a drain - POST /v2/channels/<channel>/drains { "url": "syslog://.../" }
17
+ # API> channel.create_drain(url) => Drain
18
+ # Create a session - POST /v2/sessions { "channel_id": <channel>, ... }
19
+ # API> channel.create_session(:source => ..., ps => ..., num => ..., tail => ...) => Session
20
+
21
+ # Public: A logplex channel.
22
+ #
23
+ # You generally acquire one of these objects from
24
+ # Logplex::Client#create_channel or Logplex::Client#channel
25
+ class Logplex::Channel
26
+ # Public: initialize a channel object
27
+ #
28
+ # url - the url to logplex. See Logplex::Client#new for more info.
29
+ # channel_id - the channel id, should be a number.
30
+ #
31
+ # See also: Logplex::Client#create_channel and Logplex::Client#channel
32
+ def initialize(url, channel_id)
33
+ @url = url
34
+ @channel_id = channel_id
35
+ @backend = Logplex::Client::Backends::HTTP.new(@url)
36
+ end # def initialize
37
+
38
+ # Public: Destroy a channel.
39
+ #
40
+ # This will remove a channel from logplex as well as any associated tokens or
41
+ # drains.
42
+ #
43
+ # Returns nothing.
44
+ # Raises # TODO(sissel): What errors?
45
+ def destroy
46
+ @backend.delete_channel(@channel_id)
47
+ end # def destroy
48
+
49
+ # Public: Get the channel id.
50
+ #
51
+ # Returns the channel id.
52
+ def id
53
+ return @channel_id
54
+ end # def id
55
+
56
+ # Public: Get a list of tokens associated with this channel.
57
+ #
58
+ # Note: To create a token, see Logplex::Channel#create_token
59
+ #
60
+ # Returns an Array of Logplex::Tokens
61
+ # Raises # TODO(sissel): ???
62
+ def tokens
63
+ @info ||= @backend.get_channel(@channel_id)
64
+ result = @info
65
+ return result[:tokens].collect do |info|
66
+ Logplex::Token.new(@url, @channel_id, info[:name], info[:token])
67
+ end
68
+ end # def tokens
69
+
70
+ # Public: Get a list of drains associated with this channel.
71
+ #
72
+ # Note: To create a drain, see Logplex::Channel#create_drain
73
+ #
74
+ # Returns an Array of Logplex::Drains
75
+ # Raises # TODO(sissel): ???
76
+ def drains
77
+ @info ||= @backend.get_channel(@channel_id)
78
+ result = @info
79
+ return result[:drains].collect do |info|
80
+ Logplex::Drain.new(@url, @channel_id, info[:id], info[:token], info[:url])
81
+ end
82
+ end # def drains
83
+
84
+ # Public: Create a new token for this channel.
85
+ #
86
+ # name - a String name for this token. The name is meaningful and will
87
+ # appear in drain or session output.
88
+ #
89
+ # Returns a Logplex::Token
90
+ def create_token(name)
91
+ result = @backend.create_token(@channel_id, name)
92
+ return Logplex::Token.new(@url, @channel_id, result[:name], result[:token])
93
+ end # def create_token
94
+
95
+ # Public: Create a new drain for this channel.
96
+ #
97
+ # For information on what a drain is, see Logplex::Drain
98
+ #
99
+ # Returns a Logplex::Drain
100
+ # Raises # TODO(sissel): ???
101
+ def create_drain(url=nil)
102
+ # Just create the drain, don't add any URLs to it.
103
+ # API call /channels/[channel-id]/drains/tokens
104
+ result = @backend.create_drain(@channel_id, url)
105
+ return Logplex::Drain.new(@url, @channel_id, result[:id], result[:token], url)
106
+ end # def create_drain
107
+
108
+ # Public: Create a new session for this channel.
109
+ #
110
+ # For information on what a session is, see Logplex::Session
111
+ #
112
+ # settings - a hash containing:
113
+ # :source - the token name, like "app" or "heroku" (default: nil, optional)
114
+ # :ps - the RFC5424 process id, like "web.1" (default: nil, optional)
115
+ # :num - the number of events to retrieve (default: 40, optional)
116
+ # :tail - true if you wish to follow the live event stream
117
+ # (default: false, optional)
118
+ #
119
+ # Returns a Logplex::Session
120
+ # Raises # TODO(sissel): ???
121
+ def create_session(settings={})
122
+ settings = {
123
+ :num => 40,
124
+ :tail => false
125
+ }.merge(settings)
126
+
127
+ # Currently a bug(?) in logplex that if you specify 'num' = 0, you get all
128
+ # logs. TODO(sissel): If you specify '0.1' you get zero results, so use
129
+ # that hack to hide the '0' bug until the bug is fixed.
130
+ settings[:num] = 0.1 if settings[:num] == 0
131
+
132
+ # Here's what core does: https://github.com/heroku/core/blob/master/lib/logplex.rb#L12
133
+ # TODO(sissel): generate a random 'srv' id
134
+ server_id = rand(1000)
135
+ result = @backend.create_session(@channel_id, server_id, settings)
136
+ # the foo=bar& prefix is required because of some problem with haproxy or
137
+ # how we are using it to pin http requests to backends.
138
+ # Additionally, sesssions also don't need authentication, so strip that out.
139
+ url = URI.parse(@url + result[:url] + "?" + "foo=bar&srv=#{server_id}")
140
+ url.user = nil
141
+ url.password = nil
142
+ return Logplex::Session.new(url.to_s, settings)
143
+ end # def create_session
144
+ end # class Logplex::Channel
@@ -0,0 +1,192 @@
1
+ require "multi_json"
2
+ require "restclient"
3
+ require "logplex/namespace"
4
+ require "openssl"
5
+
6
+ # A client implementation that speaks to the logplex http interface.
7
+ class Logplex::Client::Backends::HTTP
8
+ # Parent class of all Logplex-related HTTP errors.
9
+ class Error < StandardError; end
10
+
11
+ # An error indicating the call to Logplex failed due to authentication
12
+ # problems.
13
+ class Unauthorized < Error; end
14
+
15
+ # An error indicating the call to Logplex failed due to a HTTP request
16
+ # returning 404.
17
+ class NotFound < Error; end
18
+
19
+ # A new logplex backend that uses HTTP
20
+ #
21
+ # url - (string) url to logplex, like "https://user:pass@logplex.example.com/"
22
+ def initialize(url)
23
+ @url = url
24
+ end # def initialize
25
+
26
+ # Returns a hash with form:
27
+ # {:channel_id=>3639270, :tokens=>[]}
28
+ def create_channel(name, tokens = [])
29
+ json_post("/channels", {:name => name, :tokens => tokens})
30
+ end # def create_channel
31
+
32
+ # Public: Creates a channel.
33
+ #
34
+ # Returns a hash of the json response in the form:
35
+ # {:channel_id=>3639270, :tokens=>[], :drains=>[]}
36
+ def get_channel(channel_id)
37
+ json_get("/v2/channels/#{channel_id}")
38
+ end # def get_channel
39
+
40
+ # Public: Deletes a channel
41
+ def delete_channel(channel_id)
42
+ delete("/v2/channels/#{channel_id}")
43
+ end # def delete_channel
44
+
45
+ # Public: Creates a token on a given channel
46
+ def create_token(channel_id, token_name)
47
+ json_post("/v2/channels/#{channel_id}/tokens", {:name => token_name})
48
+ end # def create_token
49
+
50
+ # Public: Creates a drain for a given channel.
51
+ def create_drain(channel_id, url)
52
+ json_post("/v2/channels/#{channel_id}/drains", :url => url)
53
+ end # def create_drain
54
+
55
+ # Public: Sets the drain url for a channel+drain.
56
+ #
57
+ # channel_id - the channel id (number)
58
+ # drain_id - the drain id (string, given from {create_drain})
59
+ # url - the url to drain events to, like "syslog://host:port/"
60
+ def set_drain_url(channel_id, drain_id, url)
61
+ json_post("/v2/channels/#{channel_id}/drains/#{drain_id}", :url => url)
62
+ end # def set_drain_url
63
+
64
+ # Public: Delete a drain
65
+ #
66
+ # channel_id - the channel id (number)
67
+ # drain_id - the drain id (string, given from {create_drain})
68
+ def delete_drain(channel_id, drain_id)
69
+ delete("/v2/channels/#{channel_id}/drains/#{drain_id}")
70
+ end
71
+
72
+ # Creates a event stream session on a channel.
73
+ #
74
+ # channel_id - the channel id (number)
75
+ # server_id - the server id to use in this request
76
+ #
77
+ # Note: The 'server_id' is just used for request routing purposes. When
78
+ # creating and reading a session, you *must* use the same server_id in both
79
+ # requests. This lets any load balancer in front of logplex pin the session
80
+ # create and read to the same backend logplex session.
81
+ def create_session(channel_id, server_id, opts = {})
82
+ # The 'foo=bar&' part is due to how we use haproxy for pinning
83
+ # Also channel_id should be a string, also.
84
+ opts = opts.merge(:channel_id => channel_id.to_s)
85
+ # num needs to be a string
86
+ opts[:num] = opts[:num].to_s if opts.has_key?(:num)
87
+ # If the 'tail' opt is false, remove it.
88
+ opts.delete(:tail) if !opts[:tail]
89
+ json_post("/v2/sessions?foo=bar&srv=#{server_id}", opts)
90
+ end # def create_session
91
+
92
+ private
93
+
94
+ # Private: Do an http GET an expect JSON response.
95
+ #
96
+ # Returns the ruby-equivalent object of the JSON data.
97
+ def json_get(path, options = {})
98
+ options = { :symbolize_keys => true }.merge(options)
99
+ options.update(:accept => "application/json")
100
+ symbolize_keys = options.delete(:symbolize_keys)
101
+
102
+ body = get(path, options)
103
+ MultiJson.load(body, :symbolize_keys => symbolize_keys)
104
+ end # def json_get
105
+
106
+ # Private: Do an http POST an expect JSON response.
107
+ #
108
+ # Returns the ruby-equivalent object of the JSON data.
109
+ def json_post(path, body = {}, options = {})
110
+ options = { :symbolize_keys => true }.merge(options)
111
+ options.update(:accept => "application/json")
112
+ symbolize_keys = options.delete(:symbolize_keys)
113
+
114
+ resp = post(path, MultiJson.dump(body), options)
115
+ MultiJson.load(resp, :symbolize_keys => symbolize_keys)
116
+ end # def json_post
117
+
118
+ # Private: Do an http GET.
119
+ #
120
+ # Passes path and options to RestClient without modification.
121
+ #
122
+ # Returns the response body.
123
+ # Raises NotFound on 404
124
+ # Raises Unauthorized on authentication failure
125
+ # Raises Error on any other failure (socket error, etc)
126
+ def get(path, options = {})
127
+ with_error_translation(path) do
128
+ connection[path].get(options)
129
+ end
130
+ end
131
+
132
+ # Private: Do an http POST.
133
+ #
134
+ # Passes path and options to RestClient without modification.
135
+ #
136
+ # Returns the response body.
137
+ # Raises NotFound on 404
138
+ # Raises Unauthorized on authentication failure
139
+ # Raises Error on any other failure (socket error, etc)
140
+ def post(path, body, options = {})
141
+ with_error_translation(path) do
142
+ connection[path].post(body, options)
143
+ end
144
+ end # def post
145
+
146
+ # Private: Do an http DELETE.
147
+ #
148
+ # Passes path and options to RestClient without modification.
149
+ #
150
+ # Returns the response body.
151
+ # Raises NotFound on 404
152
+ # Raises Unauthorized on authentication failure
153
+ # Raises Error on any other failure (socket error, etc)
154
+ def delete(path, options = {})
155
+ with_error_translation(path) do
156
+ connection[path].delete(options)
157
+ end
158
+ end # def delete
159
+
160
+ # Private: Call the block and wrap expected exceptions with the http path.
161
+ #
162
+ # Returns the result of the block on success.
163
+ def with_error_translation(path)
164
+ yield
165
+ rescue RestClient::ResourceNotFound => e
166
+ raise NotFound, "The path `#{path}` could not be found."
167
+ rescue RestClient::Unauthorized
168
+ raise Unauthorized, "Logplex authentication failed"
169
+ rescue RestClient::Exception, SystemCallError, SocketError => e
170
+ message = e.respond_to?(:http_code) ? e.http_code : "#{e.class}: #{e.message}"
171
+ message = "(#{message})" if message
172
+ raise Error, "Trouble communicating with Logplex. Try again soon. #{message}".strip
173
+ end
174
+
175
+ # Private Get an instance of RestClient::Resource to use as the http interface.
176
+ #
177
+ # Subsequent calls to this will return the same object instance.
178
+ #
179
+ # Returns an instnace of RestClient::Resource
180
+ def connection
181
+ @connection ||= RestClient::Resource.new(@url,
182
+ :verify_ssl => ssl_verification() )
183
+ end # def connection
184
+
185
+ def ssl_verification
186
+ if ENV['SSL_VERIFY_NONE'] then
187
+ OpenSSL::SSL::VERIFY_NONE
188
+ else
189
+ OpenSSL::SSL::VERIFY_PEER
190
+ end
191
+ end
192
+ end # class Logplex::Client::Backends::HTTP