steno 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +14 -0
- data/lib/steno.rb +134 -0
- data/lib/steno/codec.rb +2 -0
- data/lib/steno/codec/base.rb +34 -0
- data/lib/steno/codec/json.rb +36 -0
- data/lib/steno/config.rb +56 -0
- data/lib/steno/context.rb +59 -0
- data/lib/steno/core_ext.rb +5 -0
- data/lib/steno/errors.rb +3 -0
- data/lib/steno/http_handler.rb +42 -0
- data/lib/steno/log_level.rb +24 -0
- data/lib/steno/logger.rb +177 -0
- data/lib/steno/record.rb +39 -0
- data/lib/steno/sink.rb +3 -0
- data/lib/steno/sink/base.rb +36 -0
- data/lib/steno/sink/io.rb +48 -0
- data/lib/steno/sink/syslog.rb +38 -0
- data/lib/steno/tagged_logger.rb +59 -0
- data/lib/steno/version.rb +3 -0
- data/spec/core_ext_spec.rb +15 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/barrier.rb +22 -0
- data/spec/support/null_sink.rb +17 -0
- data/spec/support/shared_context_specs.rb +7 -0
- data/spec/unit/context_spec.rb +62 -0
- data/spec/unit/http_handler_spec.rb +73 -0
- data/spec/unit/io_sink_spec.rb +26 -0
- data/spec/unit/json_codec_spec.rb +48 -0
- data/spec/unit/log_level_spec.rb +18 -0
- data/spec/unit/logger_spec.rb +91 -0
- data/spec/unit/record_spec.rb +21 -0
- data/spec/unit/steno_spec.rb +86 -0
- data/spec/unit/syslog_sink_spec.rb +27 -0
- data/spec/unit/tagged_logger_spec.rb +33 -0
- data/steno.gemspec +26 -0
- metadata +195 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 mpage
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Steno
|
2
|
+
Steno is a lightweight, modular logging library written specifically to support
|
3
|
+
Cloud Foundry.
|
4
|
+
|
5
|
+
## Concepts
|
6
|
+
|
7
|
+
Steno is composed of three main classes: loggers, sinks, and formatters. Loggers
|
8
|
+
are the main entry point for Steno. They consume user input, create structured
|
9
|
+
records, and forward said records to the configured sinks. Sinks are the
|
10
|
+
ultimate destination for log records. They transform a structured record into
|
11
|
+
a string via a formatter and then typically write the transformed string to
|
12
|
+
another transport.
|
13
|
+
|
14
|
+
## Getting started
|
15
|
+
config = Steno::Config.new(
|
16
|
+
:sinks => [Steno::Sink::IO.new(STDOUT)],
|
17
|
+
:codec => Steno::Codec::Json.new,
|
18
|
+
:context => Steno::Context::ThreadLocal.new)
|
19
|
+
|
20
|
+
Steno.init(config)
|
21
|
+
|
22
|
+
logger = Steno.logger("test")
|
23
|
+
|
24
|
+
logger.info("Hello world!")
|
25
|
+
|
26
|
+
## File a Bug
|
27
|
+
|
28
|
+
To file a bug against Cloud Foundry Open Source and its components, sign up and use our
|
29
|
+
bug tracking system: [http://cloudfoundry.atlassian.net](http://cloudfoundry.atlassian.net)
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "ci/reporter/rake/rspec"
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
|
5
|
+
desc "Run all specs"
|
6
|
+
RSpec::Core::RakeTask.new("spec") do |t|
|
7
|
+
t.rspec_opts = %w[--color --format documentation]
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Run all specs and provide output for ci"
|
11
|
+
RSpec::Core::RakeTask.new("spec:ci" => "ci:setup:rspec") do |t|
|
12
|
+
t.rspec_opts = %w[--no-color --format documentation]
|
13
|
+
end
|
14
|
+
|
data/lib/steno.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
require "thread"
|
2
|
+
|
3
|
+
require "steno/codec"
|
4
|
+
require "steno/config"
|
5
|
+
require "steno/context"
|
6
|
+
require "steno/core_ext"
|
7
|
+
require "steno/errors"
|
8
|
+
require "steno/log_level"
|
9
|
+
require "steno/logger"
|
10
|
+
require "steno/tagged_logger"
|
11
|
+
require "steno/record"
|
12
|
+
require "steno/sink"
|
13
|
+
require "steno/version"
|
14
|
+
|
15
|
+
module Steno
|
16
|
+
class << self
|
17
|
+
|
18
|
+
attr_reader :config
|
19
|
+
attr_reader :logger_regexp
|
20
|
+
|
21
|
+
# Initializes the logging system. This must be called exactly once before
|
22
|
+
# attempting to use any Steno class methods.
|
23
|
+
#
|
24
|
+
# @param [Steno::Config]
|
25
|
+
#
|
26
|
+
# @return [nil]
|
27
|
+
def init(config)
|
28
|
+
@config = config
|
29
|
+
|
30
|
+
@loggers = {}
|
31
|
+
@loggers_lock = Mutex.new
|
32
|
+
|
33
|
+
@logger_regexp = nil
|
34
|
+
@logger_regexp_level = nil
|
35
|
+
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns (and memoizes) the logger identified by name.
|
40
|
+
#
|
41
|
+
# @param [String] name
|
42
|
+
#
|
43
|
+
# @return [Steno::Logger]
|
44
|
+
def logger(name)
|
45
|
+
@loggers_lock.synchronize do
|
46
|
+
logger = @loggers[name]
|
47
|
+
|
48
|
+
if logger.nil?
|
49
|
+
level = compute_level(name)
|
50
|
+
|
51
|
+
logger = Steno::Logger.new(name, @config.sinks,
|
52
|
+
:level => level,
|
53
|
+
:context => @config.context)
|
54
|
+
|
55
|
+
@loggers[name] = logger
|
56
|
+
end
|
57
|
+
|
58
|
+
logger
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Sets all loggers whose name matches _regexp_ to _level_. Resets any
|
63
|
+
# loggers whose name matches the previous regexp but not the supplied regexp.
|
64
|
+
#
|
65
|
+
# @param [Regexp] regexp
|
66
|
+
# @param [Symbol] level
|
67
|
+
#
|
68
|
+
# @return [nil]
|
69
|
+
def set_logger_regexp(regexp, level)
|
70
|
+
@loggers_lock.synchronize do
|
71
|
+
@loggers.each do |name, logger|
|
72
|
+
if name =~ regexp
|
73
|
+
logger.level = level
|
74
|
+
elsif @logger_regexp && (name =~ @logger_regexp)
|
75
|
+
# Reset loggers affected by the old regexp but not by the new
|
76
|
+
logger.level = @config.default_log_level
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
@logger_regexp = regexp
|
81
|
+
@logger_regexp_level = level
|
82
|
+
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Clears the logger regexp, if set. Resets the level of any loggers matching
|
88
|
+
# the regex to the default log level.
|
89
|
+
#
|
90
|
+
# @return [nil]
|
91
|
+
def clear_logger_regexp
|
92
|
+
@loggers_lock.synchronize do
|
93
|
+
return if @logger_regexp.nil?
|
94
|
+
|
95
|
+
@loggers.each do |name, logger|
|
96
|
+
if name =~ @logger_regexp
|
97
|
+
logger.level = @config.default_log_level
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
@logger_regexp = nil
|
102
|
+
@logger_regexp_level = nil
|
103
|
+
end
|
104
|
+
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
# @return [Hash] Map of logger name => level
|
110
|
+
def logger_level_snapshot
|
111
|
+
@loggers_lock.synchronize do
|
112
|
+
snapshot = {}
|
113
|
+
|
114
|
+
@loggers.each { |name, logger| snapshot[name] = logger.level }
|
115
|
+
|
116
|
+
snapshot
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def compute_level(name)
|
123
|
+
if @logger_regexp && name =~ @logger_regexp
|
124
|
+
@logger_regexp_level
|
125
|
+
else
|
126
|
+
@config.default_log_level
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Initialize with an empty config. All log records will swallowed until Steno
|
133
|
+
# is re-initialized with sinks.
|
134
|
+
Steno.init(Steno::Config.new)
|
data/lib/steno/codec.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Steno
|
2
|
+
module Codec
|
3
|
+
end
|
4
|
+
end
|
5
|
+
|
6
|
+
class Steno::Codec::Base
|
7
|
+
# Encodes the supplied record as a string.
|
8
|
+
#
|
9
|
+
# @param [Hash] record
|
10
|
+
#
|
11
|
+
# @return [String]
|
12
|
+
def encode_record(record)
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Hex encodes non-printable ascii characters.
|
19
|
+
#
|
20
|
+
# @param [String] data
|
21
|
+
#
|
22
|
+
# @return [String]
|
23
|
+
def escape_nonprintable_ascii(data)
|
24
|
+
data.chars.map do |c|
|
25
|
+
ord_val = c.ord
|
26
|
+
|
27
|
+
if (ord_val > 31) && (ord_val < 127)
|
28
|
+
c
|
29
|
+
else
|
30
|
+
"\\x%02x" % [ord_val]
|
31
|
+
end
|
32
|
+
end.join
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "yajl"
|
2
|
+
|
3
|
+
require "steno/codec/base"
|
4
|
+
|
5
|
+
module Steno
|
6
|
+
module Codec
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Steno::Codec::Json < Steno::Codec::Base
|
11
|
+
def encode_record(record)
|
12
|
+
msg =
|
13
|
+
if record.message.valid_encoding?
|
14
|
+
record.message
|
15
|
+
else
|
16
|
+
# Treat the message as an arbitrary sequence of bytes.
|
17
|
+
escape_nonprintable_ascii(record.message.dup.force_encoding("BINARY"))
|
18
|
+
end
|
19
|
+
|
20
|
+
h = {
|
21
|
+
"timestamp" => record.timestamp.to_f,
|
22
|
+
"message" => msg,
|
23
|
+
"log_level" => record.log_level.to_s,
|
24
|
+
"source" => record.source,
|
25
|
+
"data" => record.data,
|
26
|
+
"thread_id" => record.thread_id,
|
27
|
+
"fiber_id" => record.fiber_id,
|
28
|
+
"process_id" => record.process_id,
|
29
|
+
"file" => record.file,
|
30
|
+
"lineno" => record.lineno,
|
31
|
+
"method" => record.method,
|
32
|
+
}
|
33
|
+
|
34
|
+
Yajl::Encoder.encode(h) + "\n"
|
35
|
+
end
|
36
|
+
end
|
data/lib/steno/config.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
require "steno/codec"
|
4
|
+
require "steno/context"
|
5
|
+
require "steno/logger"
|
6
|
+
require "steno/sink"
|
7
|
+
|
8
|
+
module Steno
|
9
|
+
end
|
10
|
+
|
11
|
+
class Steno::Config
|
12
|
+
class << self
|
13
|
+
def from_file(path, overrides = {})
|
14
|
+
h = YAML.load_file(path)
|
15
|
+
|
16
|
+
opts = {
|
17
|
+
:sinks => [],
|
18
|
+
:default_log_level => h["level"].to_sym,
|
19
|
+
}
|
20
|
+
|
21
|
+
if h["file"]
|
22
|
+
opts[:sinks] << Steno::Sink::IO.for_file(h["file"])
|
23
|
+
end
|
24
|
+
|
25
|
+
if h["syslog"]
|
26
|
+
Steno::Sink::Syslog.instance.open(h["syslog"])
|
27
|
+
opts[:sinks] << Steno::Sink::Syslog.instance
|
28
|
+
end
|
29
|
+
|
30
|
+
if opts[:sinks].empty?
|
31
|
+
opts[:sinks] << Steno::Sink::IO.new(STDOUT)
|
32
|
+
end
|
33
|
+
|
34
|
+
new(opts.merge(overrides))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :sinks
|
39
|
+
attr_reader :codec
|
40
|
+
attr_reader :context
|
41
|
+
attr_reader :default_log_level
|
42
|
+
|
43
|
+
def initialize(opts = {})
|
44
|
+
@sinks = opts[:sinks] || []
|
45
|
+
@codec = opts[:codec] || Steno::Codec::Json.new
|
46
|
+
@context = opts[:context] ||Steno::Context::Null.new
|
47
|
+
|
48
|
+
@sinks.each { |sink| sink.codec = @codec }
|
49
|
+
|
50
|
+
if opts[:default_log_level]
|
51
|
+
@default_log_level = opts[:default_log_level].to_sym
|
52
|
+
else
|
53
|
+
@default_log_level = :info
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "fiber"
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
class Fiber
|
5
|
+
def __steno_context_data__
|
6
|
+
@__steno_context_data__ ||= {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def __steno_clear_context_data__
|
10
|
+
@__steno_context_data__ = {}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Steno
|
15
|
+
end
|
16
|
+
|
17
|
+
module Steno::Context
|
18
|
+
class Base
|
19
|
+
def data
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def clear
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Null < Base
|
29
|
+
def data
|
30
|
+
{}
|
31
|
+
end
|
32
|
+
|
33
|
+
def clear
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class ThreadLocal < Base
|
39
|
+
THREAD_LOCAL_KEY = "__steno_locals__"
|
40
|
+
|
41
|
+
def data
|
42
|
+
Thread.current[THREAD_LOCAL_KEY] ||= {}
|
43
|
+
end
|
44
|
+
|
45
|
+
def clear
|
46
|
+
Thread.current[THREAD_LOCAL_KEY] = {}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class FiberLocal < Base
|
51
|
+
def data
|
52
|
+
Fiber.current.__steno_context_data__
|
53
|
+
end
|
54
|
+
|
55
|
+
def clear
|
56
|
+
Fiber.current.__steno_clear_context_data__
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/steno/errors.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require "steno"
|
2
|
+
|
3
|
+
require "grape"
|
4
|
+
|
5
|
+
module Steno
|
6
|
+
end
|
7
|
+
|
8
|
+
class Steno::HttpHandler < Grape::API
|
9
|
+
format :json
|
10
|
+
error_format :json
|
11
|
+
|
12
|
+
resource :loggers do
|
13
|
+
get :levels do
|
14
|
+
Steno.logger_level_snapshot
|
15
|
+
end
|
16
|
+
|
17
|
+
put :levels do
|
18
|
+
missing = [:regexp, :level].select { |p| !params.key?(p) }.map(&:to_s)
|
19
|
+
|
20
|
+
if !missing.empty?
|
21
|
+
error!("Missing query parameters: #{missing}", 400)
|
22
|
+
end
|
23
|
+
|
24
|
+
regexp = nil
|
25
|
+
begin
|
26
|
+
regexp = Regexp.new(params[:regexp])
|
27
|
+
rescue => e
|
28
|
+
error!("Invalid regexp", 400)
|
29
|
+
end
|
30
|
+
|
31
|
+
level = params[:level].to_sym
|
32
|
+
if !Steno::Logger::LEVELS.key?(level)
|
33
|
+
levels = Steno::Logger::LEVELS.keys.map(&:to_s)
|
34
|
+
error!("Unknown level: #{level}. Supported levels are: #{levels}", 400)
|
35
|
+
end
|
36
|
+
|
37
|
+
Steno.set_logger_regexp(regexp, level)
|
38
|
+
|
39
|
+
"ok"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|