logplex-client 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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