logplex-client 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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