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.
@@ -0,0 +1,219 @@
1
+ require 'clamp'
2
+
3
+ module Logplex
4
+ class Client
5
+
6
+ # The logplex client command-line interface
7
+ class CLI < Clamp::Command
8
+
9
+ # Generic error used for subclassing CLI-specific problems
10
+ class Error < StandardError; end
11
+
12
+ # General 'this command failed' error
13
+ class CommandFailed < Error; end
14
+
15
+ option ["-v", "--version"], :flag, "Show logplex client version" do
16
+ puts "logplex-client #{VERSION}"
17
+ exit 0
18
+ end
19
+
20
+ subcommand "channels:create", "Create a logplex channel" do
21
+ option "--logplex-url", "LOGPLEX_URL",
22
+ "The url to logplex, like 'https://user:pass@logplex.example.com/'",
23
+ :environment_variable => "LOGPLEX_URL", :required => true
24
+ parameter "NAME", "channel name"
25
+ parameter "[TOKEN] ...", "associated channel tokens"
26
+ def execute
27
+ print "creating channel... "
28
+ channel = client.create_channel(name)
29
+ puts "done"
30
+
31
+ puts "channel id: #{channel.id}"
32
+ if !token_list.nil? && !token_list.empty?
33
+ token_list.each do |token|
34
+ puts "creating token (#{token})"
35
+ channel.create_token(token)
36
+ end
37
+ end
38
+ rescue => e
39
+ failure(e)
40
+ end
41
+ end
42
+
43
+ subcommand "channels:get", "Get channel info" do
44
+ option "--logplex-url", "LOGPLEX_URL",
45
+ "The url to logplex, like 'https://user:pass@logplex.example.com/'",
46
+ :environment_variable => "LOGPLEX_URL", :required => true
47
+ parameter "CHANNEL", "channel id"
48
+ def execute
49
+ print "getting channel (#{channel})... "
50
+ chan = client.channel(channel)
51
+ puts "done"
52
+ puts
53
+ puts "id: #{chan.id}"
54
+ puts "no tokens" if chan.tokens.empty?
55
+ chan.tokens.each do |token|
56
+ puts "token: #{token.name} (#{token.id})"
57
+ end
58
+ puts "no drains" if chan.drains.empty?
59
+ chan.drains.each do |drain|
60
+ puts "drain: #{drain.drain_url} (#{drain.id})"
61
+ end
62
+ rescue => e
63
+ failure(e)
64
+ end
65
+ end
66
+
67
+ subcommand "channels:delete", "Delete a channel and associated drains/tokens" do
68
+ option "--logplex-url", "LOGPLEX_URL",
69
+ "The url to logplex, like 'https://user:pass@logplex.example.com/'",
70
+ :environment_variable => "LOGPLEX_URL", :required => true
71
+ parameter "CHANNEL", "channel name or id"
72
+ def execute
73
+ print "deleting channel... "
74
+ client.channel(channel).destroy
75
+ puts "done"
76
+ rescue => e
77
+ failure(e)
78
+ end
79
+ end
80
+
81
+ subcommand "tokens:create", "Add a token to a channel" do
82
+ option "--logplex-url", "LOGPLEX_URL",
83
+ "The url to logplex, like 'https://user:pass@logplex.example.com/'",
84
+ :environment_variable => "LOGPLEX_URL", :required => true
85
+ parameter "CHANNEL", "channel id"
86
+ parameter "NAME", "token name"
87
+ def execute
88
+ print "creating token... "
89
+ chan = client.channel(channel)
90
+ token = chan.create_token(name)
91
+ puts "done"
92
+ puts "#{name} => #{token.id}"
93
+ rescue => e
94
+ failure(e)
95
+ end
96
+ end
97
+
98
+ subcommand "tokens:delete", "Remove a token from a channel" do
99
+ option "--logplex-url", "LOGPLEX_URL",
100
+ "The url to logplex, like 'https://user:pass@logplex.example.com/'",
101
+ :environment_variable => "LOGPLEX_URL", :required => true
102
+ parameter "CHANNEL", "channel id"
103
+ parameter "NAME", "token name"
104
+ def execute
105
+ # TODO(sissel): Logplex doesn't appear to have (or document?) this functionality.
106
+ # per http://logplex.herokuapp.com/ as of 2012/04/20
107
+ $stderr.puts "Logplex currently does not implement this functionality"
108
+ return
109
+
110
+ #found = false
111
+ #client.channel(channel).tokens.each do |token|
112
+ #if token.name == name
113
+ #puts "deleting token... "
114
+ #token.destroy
115
+ #found = true
116
+ #end
117
+ #end
118
+ #puts "no token found" if !found
119
+ rescue => e
120
+ failure(e)
121
+ end
122
+ end
123
+
124
+ subcommand "drains:create", "Add a drain to a channel" do
125
+ option "--logplex-url", "LOGPLEX_URL",
126
+ "The url to logplex, like 'https://user:pass@logplex.example.com/'",
127
+ :environment_variable => "LOGPLEX_URL", :required => true
128
+ parameter "CHANNEL", "channel id"
129
+
130
+ # For now, due to a bug (or misdocumented feature) in logplex, a drain
131
+ # url is required at creation-time.
132
+ parameter "URL", "drain url (like 'syslog://..../')"
133
+
134
+ def execute
135
+ print "creating drain... "
136
+ chan = client.channel(channel)
137
+ drain = chan.create_drain(url)
138
+ puts "done"
139
+ rescue => e
140
+ failure(e)
141
+ end
142
+ end
143
+
144
+ subcommand "drains:delete", "Remove a drain from a channel" do
145
+ option "--logplex-url", "LOGPLEX_URL",
146
+ "The url to logplex, like 'https://user:pass@logplex.example.com/'",
147
+ :environment_variable => "LOGPLEX_URL", :required => true
148
+ parameter "CHANNEL", "channel id"
149
+ parameter "DRAIN", "drain id or token"
150
+ def execute
151
+ found = false
152
+ channel = client.channel(channel)
153
+ channel.drains.each do |d|
154
+ if d.token == drain or d.id == drain
155
+ print "deleting drain... "
156
+ d.destroy
157
+ puts "done"
158
+ found = true
159
+ end
160
+ end
161
+ puts "no drain found" if !found
162
+ rescue => e
163
+ failure(e)
164
+ end
165
+ end
166
+
167
+ subcommand "channels:logs", "fetch channel logs" do
168
+ option "--logplex-url", "LOGPLEX_URL",
169
+ "The url to logplex, like 'https://user:pass@logplex.example.com/'",
170
+ :environment_variable => "LOGPLEX_URL", :required => true
171
+ parameter "CHANNEL", "channel id"
172
+ option ["--source", "-s"], "SOURCE", "log source"
173
+ option ["--ps", "-p"], "PS", "log process"
174
+ option ["--num", "-n"], "NUM", "max number of logs to fetch"
175
+ option ["--tail", "-t"], :flag, "maintain a tail session of log stream"
176
+ option ["--chunk-size", "-c"], "CHUNKS_SIZE", "response chunk size", :default => Logplex::Session::DEFAULT_CHUNK_SIZE
177
+
178
+ def execute
179
+ opts = {}
180
+ opts[:source] = source if source
181
+ opts[:ps] = ps if ps
182
+ opts[:num] = num.to_i if num
183
+ opts[:tail] = true if tail?
184
+ opts[:chunk_size] = chunk_size.to_i if chunk_size
185
+ chan = client.channel(channel)
186
+ session = chan.create_session(opts)
187
+ session.each_event do |event|
188
+ puts event
189
+ end
190
+ rescue => e
191
+ failure(e)
192
+ end
193
+ end
194
+
195
+ private
196
+
197
+ # Private: Get a logplex client instance.
198
+ #
199
+ # Subsequent calls to this method wil return the same object.
200
+ #
201
+ # Returns a Logplex::Client
202
+ def client
203
+ @client ||= Logplex::Client.new(logplex_url)
204
+ end # def client
205
+
206
+ # Private: Emit a failure.
207
+ def failure(e)
208
+ puts "failed"
209
+
210
+ if e.is_a?(Logplex::Client::Backends::HTTP::NotFound)
211
+ puts "! error: Object not found (#{e.to_s})"
212
+ else
213
+ puts "! error: #{e.to_s}"
214
+ end
215
+ exit(1)
216
+ end # def failure
217
+ end # class Logplex::Client::CLI
218
+ end # class Logplex::Client
219
+ end # module Logplex
@@ -0,0 +1,6 @@
1
+ module Logplex
2
+ class Client
3
+ # The version of this library.
4
+ VERSION = "0.1.4"
5
+ end
6
+ end
@@ -0,0 +1,80 @@
1
+ require "logplex/namespace"
2
+ require "logplex/client/version"
3
+ require "logplex/client/backends/http"
4
+ require "logplex/channel"
5
+ require "logplex/session"
6
+
7
+ # Public: a logplex client.
8
+ #
9
+ # This class allows you to access logplex as a client; reading and writing
10
+ # logs, creating channels, tokens, etc.
11
+ #
12
+ # For logplex's HTTP API, see http://logplex.herokuapp.com
13
+ #
14
+ # Examples
15
+ #
16
+ # client = Logplex::Client.new("https://user:pass@logplex.example.com/")
17
+ # channel = client.create_channel("my-example-channel")
18
+ # TODO(sissel): Complete this example.
19
+ class Logplex::Client
20
+ # Public: Initialize a client pointing at a given url.
21
+ #
22
+ # url - a url (including user+pass) to your logplex service
23
+ #
24
+ # Examples
25
+ #
26
+ # Logplex::Client.new("https://user:pass@logplex.heroku.com/")
27
+ def initialize(url)
28
+ @url = url
29
+ @backend = Logplex::Client::Backends::HTTP.new(@url)
30
+ end # def initialize
31
+
32
+ # Public: Create a channel.
33
+ #
34
+ # Note: the 'name' of the channel lives in a global namespace. Choose wisely.
35
+ # Note: You must save the channel id if you wish to use the channel later.
36
+ #
37
+ # name - the string name to give to the channel being created.
38
+ #
39
+ # Examples
40
+ #
41
+ # client.create_channel("my-example-channel")
42
+ #
43
+ # Returns a {Logplex::Channel}
44
+ # Raises TODO(sissel): Raises what?
45
+ def create_channel(name)
46
+ # TODO(sissel): Call the API
47
+ result = @backend.create_channel(name)
48
+ return Logplex::Channel.new(@url, result[:channel_id])
49
+ end # def create_channel
50
+
51
+ # Public: Get a {Logplex::Channel} instance with a given id.
52
+ #
53
+ # If you have already created a channel, this is how you access it later.
54
+ #
55
+ # channel_id - the channel id (number)
56
+ #
57
+ # Returns a {Logplex::Channel}
58
+ # Raises TODO(sissel): ??? if the channel is not found
59
+ def channel(channel_id)
60
+ # This will throw an exception if it doesn't exist.
61
+ result = @backend.get_channel(channel_id)
62
+ chan = Logplex::Channel.new(@url, channel_id)
63
+ # Hack for now to push the result of get_channel into the Channel.
64
+ chan.instance_eval { @info = result }
65
+ return chan
66
+ end # def channel
67
+
68
+ # Public: Get a {Logplex::Session} instance with a given id.
69
+ #
70
+ # If you have already created a session, this is how you access it later.
71
+ #
72
+ # session_id - the session id (number)
73
+ #
74
+ # Returns a {Logplex::Session}
75
+ # Raises TODO(sissel): ??? if the session is not found
76
+ def session(session_id)
77
+ raise NotImplemented
78
+ end # def session
79
+ end # class Logplex::Client
80
+
@@ -0,0 +1,45 @@
1
+ require "logplex/namespace"
2
+
3
+ # Operations on a Logplex::Drain:
4
+ # Add a url to a drain: POST /v2/channels/<channel>/drains/<drain> { "url": ... }
5
+ # drain.use(url) ### I like 'use' better than 'drain.url = "url"'
6
+ # Delete a drain: DELETE /v2/channels/<channel>/drains/<drain>
7
+ # drain.destroy
8
+ #
9
+ class Logplex::Drain
10
+ def initialize(url, channel_id, drain_id, drain_token, drain_url=nil)
11
+ @url = url
12
+ @channel_id = channel_id
13
+ @id = drain_id
14
+ @token = drain_token
15
+ @drain_url = drain_url
16
+ @backend = Logplex::Client::Backends::HTTP.new(@url)
17
+ end # def initialize
18
+
19
+ # Public: Destroy this drain.
20
+ #
21
+ # Returns nothing
22
+ # Raises TODO(sissel): document exceptions
23
+ def destroy
24
+ @backend.delete_drain(@channel_id, @drain_id)
25
+ end # def destroy
26
+
27
+ # Public: Set the output of this drain to the given drain_url.
28
+ #
29
+ # drain_url - the url to drain logs to. Usually of form syslog://host:port/
30
+ #
31
+ # Returns nothing
32
+ # Raises TODO(sissel): Document exceptions
33
+ def drain_url=(drain_url)
34
+ # I don't really like this API feel, doing RPC calls in a setter feels like
35
+ # it breaks expectations.
36
+ @backend.set_drain_url(@channel_id, @drain_id, drain_url)
37
+ @target = drain_url
38
+ end # def use
39
+
40
+ # Public: Get the url for this drain.
41
+ attr_reader :drain_url
42
+
43
+ # Public: Get the id for this drain
44
+ attr_reader :id
45
+ end # class Logplex::Drain
@@ -0,0 +1,79 @@
1
+ require "logplex/namespace"
2
+
3
+ # A logging emitter. Best to create this with {Logplex::Token#emitter}
4
+ class Logplex::Emitter
5
+
6
+ # Default methods to private, see bottom of class for public method
7
+ # declarations
8
+ private
9
+
10
+ # Public: Create an emitter with the given target address and token.
11
+ def initialize(address, token)
12
+ @address = address
13
+ @token = token
14
+
15
+ connect
16
+ end # def initialize
17
+
18
+ # Public: Emit an event.
19
+ #
20
+ # procid - the process id emitting this message. A string, like "web.1"
21
+ # message - the message to emit in this event.
22
+ def emit(procid, message)
23
+ # FRAME: LENGTH PAYLOAD
24
+ # LENGTH: decimal value of the length of the payload
25
+ payload = serialize_syslogish(13, Time.now.strftime("%Y-%m-%dT%H:%M:%S%z"),
26
+ Socket.gethostname, @token, procid,
27
+ # 'message id' is meaningless to us
28
+ "-",
29
+ # This extra "- " is due to a bug in logplex.
30
+ "- #{message}")
31
+ event = "#{payload.size} #{payload}"
32
+ @socket.syswrite(event)
33
+ end # def emit
34
+
35
+
36
+ # Private: connect to the remote address
37
+ #
38
+ # Returns nothing
39
+ # Raises TODO(sissel): ???
40
+ def connect
41
+ # TODO(sissel): Handle errors/reconnections
42
+ @socket.close unless @socket.nil?
43
+ host, port = @address.split(":")
44
+ @socket = TCPSocket.new(host, port.to_i)
45
+ end # def connect
46
+
47
+ # Private: serialize an RFC5424 message given some parameters
48
+ #
49
+ # pri - the syslog priority (this is facility combined with severity, number)
50
+ # timestamp - the RFC3339 timestamp (string)
51
+ # host - the hostname generating this message (string)
52
+ # appname - the application generating this message (string)
53
+ # procid - the process's id who is generating this message (string)
54
+ # msgid - the message id, usually just "-" (string)
55
+ # msg - the message (string)
56
+ #
57
+ # Returns the string encoding of an RFC5424 message based on the given
58
+ # arguments
59
+ def serialize_syslogish(pri, timestamp, host, appname, procid, msgid, msg)
60
+ # This protocol is RFC5424 layered on the message framing portion of
61
+ # RFC5425 (no ssl/tls)
62
+ #
63
+ # This method implements only the event formatting, not the framing.
64
+ #
65
+ # It is roughly:
66
+ # PAYLOAD: <PRI>VERSION TIMESTAMP HOST APPNAME PROCID MSGID MSG
67
+ # PRI: 0-191 (syslog pri combination of severity * 8 + facility
68
+ # VERSION: 1
69
+ # TIMESTAMP: RFC3339 time
70
+ # HOST: hostname
71
+ # APPNAME: the logplex channel token (t.<UUID>)
72
+ # PROCID: the process id, on heroku this is usually something like 'web.9'
73
+ # MSGID: usually null as "-"
74
+ return ["<#{pri}>1", timestamp, host, appname, procid, msgid, msg].join(" ")
75
+ end # def serialize_syslogish
76
+
77
+ # Explicitly declare methods public.
78
+ public(:initialize, :emit)
79
+ end # class Logstash::Emitter
@@ -0,0 +1,8 @@
1
+ require "logplex/namespace"
2
+
3
+ # Public: An event received from a logplex session.
4
+ #
5
+ # This class is not well-thought-out yet.
6
+ class Logplex::Event
7
+ attr_accessor :time, :token, :process, :message
8
+ end # class Logplex::Event
@@ -0,0 +1,8 @@
1
+ # :nodoc:
2
+ module Logplex
3
+ # :nodoc:
4
+ class Client
5
+ # :nodoc:
6
+ module Backends; end
7
+ end
8
+ end # module Logplex
@@ -0,0 +1,72 @@
1
+ require "logplex/namespace"
2
+ require "excon"
3
+
4
+ # Operations on a Logplex::Session:
5
+ # Get logs for a session - GET /sessions/<session>
6
+ # API> session.each_event do |event|
7
+ # # 'event' should be a nice object representation, not a string.
8
+ # of a session
9
+ # end
10
+ #
11
+
12
+ # Public: A logplex event session.
13
+ #
14
+ # This class allows you to read events from logplex.
15
+ #
16
+ # You generally acquire an instance of this class through
17
+ # Logplex::Channel#create_session
18
+ class Logplex::Session
19
+ DEFAULT_CHUNK_SIZE = 1024
20
+
21
+ # Public: Initialize a logplex session.
22
+ #
23
+ # url - the url to the session, usually of the form:
24
+ # https://user:pass@logplex/sessions/session-id
25
+ #
26
+ # session_settings - a hash of arguments; see
27
+ # {Logplex::Channel#create_session} for values.
28
+ def initialize(url, session_settings)
29
+ @url = url
30
+
31
+ if session_settings[:tail]
32
+ @tail = true
33
+ else
34
+ @tail = false
35
+ @limit = session_settings[:num]
36
+ end
37
+ @chunk_size = session_settings[:chunk_size] || DEFAULT_CHUNK_SIZE
38
+ end # def initialize
39
+
40
+ # Public: iterate over events received from this logplex session
41
+ #
42
+ # Yields Logplex::Event objects, one per event.
43
+ # Returns nothing
44
+ # Raises TODO(sissel): ???
45
+ def each_event(&block)
46
+ connection = Excon.new(@url, :chunk_size => @chunk_size)
47
+
48
+ @buffer = ""
49
+ @event_count = 0
50
+
51
+ process_response = lambda do |chunk, remaining_bytes, total_bytes|
52
+ @buffer += chunk
53
+
54
+ events = @buffer.split("\n")
55
+ if @buffer[-1] != "\n"
56
+ @buffer = events.last
57
+ events = events[0 .. -2]
58
+ else
59
+ @buffer = ""
60
+ end
61
+
62
+ events.each do |line|
63
+ @event_count += 1
64
+ block.call(line)
65
+ return if !@tail && @limit < @event_count
66
+ end
67
+ end
68
+
69
+ response = connection.get(:response_block => process_response)
70
+ raise "Error: #{response.body}" if response.status != 200
71
+ end # def each_block
72
+ end # class Logplex::Session
@@ -0,0 +1,42 @@
1
+ require "logplex/namespace"
2
+ require "logplex/emitter"
3
+
4
+ # Operations on a Logplex::Token:
5
+ # Publish an event:
6
+ # API> token.publish(message)
7
+ #
8
+ #
9
+
10
+ # A logplex token is required for publishing events to logplex.
11
+ #
12
+ # To create a token, see Logplex::Channel#create_token.
13
+ class Logplex::Token
14
+ # Public: initialize a Logplex::Token
15
+ #
16
+ # url - the url to logplex; see Logplex::Client.new for more info
17
+ # channel_id - the channel id to create a token on; see Logplex::Channel for
18
+ # more info
19
+ # name - the string name of this token. This name will appear in logplex
20
+ # output (drains and log sessions) as the application name.
21
+ # token_id - the token; usually in the form 't.SOME-UUID-VALUE'
22
+ def initialize(url, channel_id, name, token_id)
23
+ @url = url
24
+ @channel_id = channel_id
25
+ @name = name
26
+ @id = token_id
27
+ end # def initialize
28
+
29
+ attr_accessor :name
30
+ attr_accessor :id
31
+ attr_reader :channel_id
32
+ attr_reader :url
33
+
34
+ # Public: Get an emitter suitable for publishing events with this token.
35
+ #
36
+ # Returns a Logplex::Emitter
37
+ def emitter
38
+ address = [URI.parse(@url).host, 601].join(":")
39
+ @emitter ||= Logplex::Emitter.new(address, @id)
40
+ return @emitter
41
+ end # def emitter
42
+ end # class Logplex::Token
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require File.expand_path("../lib/logplex/client/version", __FILE__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.authors = ["Jacob Vorreuter", "Jordan Sissel"]
7
+ gem.email = ["jacob.vorreuter@gmail.com", "jls@heroku.com"]
8
+ gem.description = %q{A client and library for Logplex}
9
+ gem.summary = %q{A client and library for Logplex}
10
+ gem.homepage = "https://github.com/heroku/logplex-client"
11
+
12
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
13
+ gem.files = `git ls-files`.split("\n")
14
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ gem.name = "logplex-client"
16
+ gem.require_paths = ["lib"]
17
+ gem.version = Logplex::Client::VERSION
18
+
19
+ # For command line flag parsing, etc.
20
+ gem.add_dependency "clamp", "~> 0.4.0"
21
+
22
+ # For json parsing
23
+ gem.add_dependency "multi_json"
24
+
25
+ # For HTTP stuff that RestClient can't handle
26
+ gem.add_dependency "excon", "~> 0.20.1"
27
+
28
+ # For general HTTP access to REST-ish APIs
29
+ gem.add_dependency "rest-client"
30
+ end
@@ -0,0 +1,41 @@
1
+ require "rubygems"
2
+ require "yard"
3
+
4
+ class MissingDocumentation < StandardError; end
5
+
6
+ describe "this project" do
7
+ before do
8
+ # Use YARD to parse all ruby files found in '../lib'
9
+ libdir = File.join(File.dirname(__FILE__), "..", "lib")
10
+ YARD::Registry.load(Dir.glob(File.join(libdir, "**", "*.rb")))
11
+ @registry = YARD::Registry.all
12
+ end
13
+
14
+ it "must have all classes, modules, and constants documented" do
15
+ # YARD's parser works best in ruby 1.9.x, so skip 1.8.x
16
+ skip if RUBY_VERSION < "1.9.2"
17
+ # Note, the 'find the undocumented things' code here is
18
+ # copied mostly from: YARD 0.7.5's lib/yard/cli/stats.rb
19
+ #
20
+ # Find all undocumented classes, modules, and constants
21
+ undocumented = @registry.select do |o|
22
+ [:class, :module, :constant].include?(o.type) && o.docstring.blank?
23
+ end
24
+
25
+ # Find all undocumented methods
26
+ methods = @registry.select { |m| m.type == :method }
27
+ methods.reject! { |m| m.is_alias? || !m.is_explicit? }
28
+ undocumented += methods.select do |m|
29
+ m.docstring.blank? && !m.overridden_method
30
+ end
31
+
32
+ if (undocumented.length > 0)
33
+ message = ["The following are not documented"]
34
+ undocumented.each do |o|
35
+ message << "* #{o.type.to_s} #{o.to_s} <#{o.file}:#{o.line}>"
36
+ end
37
+
38
+ raise MissingDocumentation.new(message.join("\n"))
39
+ end
40
+ end
41
+ end