steno-capi 1.3.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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +7136 -0
  3. data/README.md +78 -0
  4. data/Rakefile +15 -0
  5. data/bin/steno-prettify +99 -0
  6. data/lib/steno.rb +133 -0
  7. data/lib/steno/codec.rb +2 -0
  8. data/lib/steno/codec/base.rb +34 -0
  9. data/lib/steno/codec/json.rb +49 -0
  10. data/lib/steno/config.rb +101 -0
  11. data/lib/steno/context.rb +59 -0
  12. data/lib/steno/core_ext.rb +11 -0
  13. data/lib/steno/errors.rb +3 -0
  14. data/lib/steno/json_prettifier.rb +131 -0
  15. data/lib/steno/log_level.rb +24 -0
  16. data/lib/steno/logger.rb +174 -0
  17. data/lib/steno/record.rb +41 -0
  18. data/lib/steno/sink.rb +6 -0
  19. data/lib/steno/sink/base.rb +38 -0
  20. data/lib/steno/sink/counter.rb +44 -0
  21. data/lib/steno/sink/eventlog.rb +46 -0
  22. data/lib/steno/sink/fluentd.rb +31 -0
  23. data/lib/steno/sink/io.rb +72 -0
  24. data/lib/steno/sink/syslog.rb +62 -0
  25. data/lib/steno/tagged_logger.rb +59 -0
  26. data/lib/steno/version.rb +3 -0
  27. data/spec/spec_helper.rb +6 -0
  28. data/spec/support/barrier.rb +22 -0
  29. data/spec/support/null_sink.rb +17 -0
  30. data/spec/support/shared_context_specs.rb +7 -0
  31. data/spec/unit/config_spec.rb +229 -0
  32. data/spec/unit/context_spec.rb +62 -0
  33. data/spec/unit/core_ext_spec.rb +38 -0
  34. data/spec/unit/json_codec_spec.rb +68 -0
  35. data/spec/unit/json_prettifier_spec.rb +84 -0
  36. data/spec/unit/log_level_spec.rb +19 -0
  37. data/spec/unit/logger_spec.rb +101 -0
  38. data/spec/unit/record_spec.rb +30 -0
  39. data/spec/unit/sink/counter_spec.rb +27 -0
  40. data/spec/unit/sink/eventlog_spec.rb +41 -0
  41. data/spec/unit/sink/fluentd_spec.rb +46 -0
  42. data/spec/unit/sink/io_spec.rb +111 -0
  43. data/spec/unit/sink/syslog_spec.rb +75 -0
  44. data/spec/unit/steno_spec.rb +86 -0
  45. data/spec/unit/tagged_logger_spec.rb +35 -0
  46. data/steno-capi.gemspec +39 -0
  47. metadata +179 -0
@@ -0,0 +1,78 @@
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
+ ## Configuration
15
+
16
+ To use steno, you must configure one or more 'sinks', a 'codec' and a 'context'.
17
+ If you don't provide a codec, steno will encode your logs as JSON.
18
+
19
+ For example:
20
+
21
+ config = Steno::Config.new(
22
+ :sinks => [Steno::Sink::IO.new(STDOUT)],
23
+ :codec => Steno::Codec::Json.new,
24
+ :context => Steno::Context::ThreadLocal.new)
25
+
26
+ ### from YAML file
27
+
28
+ Alternatively, Steno can read its configuration from a YAML file in the following format:
29
+
30
+ ```yaml
31
+ # config.yml
32
+ ---
33
+ logging:
34
+ file: /some/path # Optional - path a log file
35
+ max_retries: 3 # Optional - number of times to retry if a file write fails.
36
+ syslog: some_syslog.id # Optional - only works on *nix systems
37
+ eventlog: true # Optional - only works on Windows
38
+ fluentd: # Optional
39
+ host: fluentd.host
40
+ port: 9999
41
+ level: debug # Optional - Minimum log level that will be written.
42
+ # Defaults to 'info'
43
+ ```
44
+ Then, in your code:
45
+ ```ruby
46
+ config = Steno::Config.from_file("path/to/config.yml")
47
+ ```
48
+
49
+ With this configuration method, if neither `file`, `syslog` or `fluentd` are provided,
50
+ steno will use its stdout as its sink. Also, note that the top-level field `logging` is required.
51
+
52
+ ### from Hash
53
+
54
+ As a third option, steno can be configured using a hash with the same structure as the above
55
+ YAML file (without the top-level `logging` key):
56
+ ```ruby
57
+ config = Steno::Config.from_hash(config_hash)
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ Steno.init(config)
63
+ logger = Steno.logger("test")
64
+ logger.info("Hello world!")
65
+
66
+ ### Log levels
67
+
68
+ LEVEL NUMERIC RANKING
69
+ off 0
70
+ fatal 1
71
+ error 5
72
+ warn 10
73
+ info 15
74
+ debug 16
75
+ debug1 17
76
+ debug2 18
77
+ all 30
78
+
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env rake
2
+ require "rspec/core/rake_task"
3
+
4
+ task :default => :spec
5
+
6
+ desc "Run all specs"
7
+ RSpec::Core::RakeTask.new("spec") do |t|
8
+ t.rspec_opts = %w[--color --format documentation]
9
+ end
10
+
11
+ desc "Run all specs and provide output for ci"
12
+ RSpec::Core::RakeTask.new("spec:ci" => "ci:setup:rspec") do |t|
13
+ t.rspec_opts = %w[--no-color --format documentation]
14
+ end
15
+
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "optparse"
4
+ require "yajl"
5
+
6
+ require "steno/json_prettifier"
7
+
8
+ Signal.trap("INT") { exit }
9
+
10
+ def prettify_io(io, prettifier, ignore_parse_error = false)
11
+ lineno = 1
12
+ json_regex = '([^{]*)({.*})'
13
+
14
+ io.sync = true
15
+
16
+ io.each_line do |line|
17
+ begin
18
+ match_data = line.match(json_regex) || []
19
+ prefix = match_data[1]
20
+ json = match_data[2]
21
+
22
+ prettified = prettifier.prettify_line(json)
23
+ print prefix + prettified
24
+ rescue Steno::JsonPrettifier::ParseError => e
25
+ if ignore_parse_error
26
+ print line
27
+ else
28
+ STDERR.puts "steno-prettify: Malformed json at line #{lineno}, '#{line.strip}'"
29
+ end
30
+ end
31
+
32
+ lineno += 1
33
+ end
34
+ end
35
+
36
+ def prettify_file(path, prettifier, ignore_parse_error = false)
37
+ begin
38
+ f = File.open(path, "r")
39
+ rescue => e
40
+ STDERR.puts "steno-prettify: Failed opening '#{path}', #{e}"
41
+ return
42
+ end
43
+
44
+ prettify_io(f, prettifier, ignore_parse_error)
45
+
46
+ ensure
47
+ f.close if f
48
+ end
49
+
50
+ excluded_fields = []
51
+ ignore_parse_error = false
52
+
53
+ option_parser = OptionParser.new do |op|
54
+ op.banner =<<EOT
55
+ Usage: steno-prettify [OPTS] [FILE...]
56
+
57
+ Parses json formatted log lines from FILE(s), or stdin,
58
+ and displays a more human friendly version of each line to stdout.
59
+
60
+ Examples (shamelessly stolen from `man cat`):
61
+
62
+ steno-prettify f - g
63
+ Prettify f's contents, then standard input, then g's contents.
64
+
65
+ steno-prettify
66
+ Prettify contents of stdin.
67
+
68
+ Options:
69
+ EOT
70
+
71
+ op.on("-h", "--help", "Display help") do
72
+ puts op
73
+ exit
74
+ end
75
+
76
+ op.on("-a", "--align", "Omit location and data in order to provide well-aligned logs") do
77
+ excluded_fields = %w[location data]
78
+ end
79
+
80
+ op.on("-s", "--no-messages", "Donot complain about errors in parsing logs") do
81
+ ignore_parse_error = true
82
+ end
83
+ end
84
+
85
+ option_parser.parse!(ARGV)
86
+
87
+ STDOUT.sync = true
88
+ prettifier = Steno::JsonPrettifier.new(excluded_fields)
89
+
90
+ inputs = ARGV.dup
91
+ inputs << "-" if inputs.empty?
92
+
93
+ inputs.each do |path|
94
+ if path == "-"
95
+ prettify_io(STDIN, prettifier, ignore_parse_error)
96
+ else
97
+ prettify_file(path, prettifier, ignore_parse_error)
98
+ end
99
+ end
@@ -0,0 +1,133 @@
1
+ require "thread"
2
+
3
+ require "steno/codec"
4
+ require "steno/config"
5
+ require "steno/context"
6
+ require "steno/errors"
7
+ require "steno/log_level"
8
+ require "steno/logger"
9
+ require "steno/tagged_logger"
10
+ require "steno/record"
11
+ require "steno/sink"
12
+ require "steno/version"
13
+
14
+ module Steno
15
+ class << self
16
+
17
+ attr_reader :config
18
+ attr_reader :logger_regexp
19
+
20
+ # Initializes the logging system. This must be called exactly once before
21
+ # attempting to use any Steno class methods.
22
+ #
23
+ # @param [Steno::Config]
24
+ #
25
+ # @return [nil]
26
+ def init(config)
27
+ @config = config
28
+
29
+ @loggers = {}
30
+ @loggers_lock = Mutex.new
31
+
32
+ @logger_regexp = nil
33
+ @logger_regexp_level = nil
34
+
35
+ nil
36
+ end
37
+
38
+ # Returns (and memoizes) the logger identified by name.
39
+ #
40
+ # @param [String] name
41
+ #
42
+ # @return [Steno::Logger]
43
+ def logger(name)
44
+ @loggers_lock.synchronize do
45
+ logger = @loggers[name]
46
+
47
+ if logger.nil?
48
+ level = compute_level(name)
49
+
50
+ logger = Steno::Logger.new(name, @config.sinks,
51
+ :level => level,
52
+ :context => @config.context)
53
+
54
+ @loggers[name] = logger
55
+ end
56
+
57
+ logger
58
+ end
59
+ end
60
+
61
+ # Sets all loggers whose name matches _regexp_ to _level_. Resets any
62
+ # loggers whose name matches the previous regexp but not the supplied regexp.
63
+ #
64
+ # @param [Regexp] regexp
65
+ # @param [Symbol] level
66
+ #
67
+ # @return [nil]
68
+ def set_logger_regexp(regexp, level)
69
+ @loggers_lock.synchronize do
70
+ @loggers.each do |name, logger|
71
+ if name =~ regexp
72
+ logger.level = level
73
+ elsif @logger_regexp && (name =~ @logger_regexp)
74
+ # Reset loggers affected by the old regexp but not by the new
75
+ logger.level = @config.default_log_level
76
+ end
77
+ end
78
+
79
+ @logger_regexp = regexp
80
+ @logger_regexp_level = level
81
+
82
+ nil
83
+ end
84
+ end
85
+
86
+ # Clears the logger regexp, if set. Resets the level of any loggers matching
87
+ # the regex to the default log level.
88
+ #
89
+ # @return [nil]
90
+ def clear_logger_regexp
91
+ @loggers_lock.synchronize do
92
+ return if @logger_regexp.nil?
93
+
94
+ @loggers.each do |name, logger|
95
+ if name =~ @logger_regexp
96
+ logger.level = @config.default_log_level
97
+ end
98
+ end
99
+
100
+ @logger_regexp = nil
101
+ @logger_regexp_level = nil
102
+ end
103
+
104
+ nil
105
+ end
106
+
107
+
108
+ # @return [Hash] Map of logger name => level
109
+ def logger_level_snapshot
110
+ @loggers_lock.synchronize do
111
+ snapshot = {}
112
+
113
+ @loggers.each { |name, logger| snapshot[name] = logger.level }
114
+
115
+ snapshot
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def compute_level(name)
122
+ if @logger_regexp && name =~ @logger_regexp
123
+ @logger_regexp_level
124
+ else
125
+ @config.default_log_level
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ # Initialize with an empty config. All log records will swallowed until Steno
132
+ # is re-initialized with sinks.
133
+ Steno.init(Steno::Config.new)
@@ -0,0 +1,2 @@
1
+ require "steno/codec/base"
2
+ require "steno/codec/json"
@@ -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,49 @@
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
+
12
+ def initialize(opts = {})
13
+ @iso8601_timestamps = opts[:iso8601_timestamps] || false
14
+ end
15
+
16
+ def encode_record(record)
17
+ msg =
18
+ if record.message.valid_encoding?
19
+ record.message
20
+ else
21
+ # Treat the message as an arbitrary sequence of bytes.
22
+ escape_nonprintable_ascii(record.message.dup.force_encoding("BINARY"))
23
+ end
24
+
25
+ h = {
26
+ "timestamp" => record.timestamp.to_f,
27
+ "message" => msg,
28
+ "log_level" => record.log_level.to_s,
29
+ "source" => record.source,
30
+ "data" => record.data,
31
+ "thread_id" => record.thread_id,
32
+ "fiber_id" => record.fiber_id,
33
+ "process_id" => record.process_id,
34
+ "file" => record.file,
35
+ "lineno" => record.lineno,
36
+ "method" => record.method,
37
+ }
38
+
39
+ if iso8601_timestamps?
40
+ h["timestamp"] = Time.at(record.timestamp).utc.iso8601(6)
41
+ end
42
+
43
+ Yajl::Encoder.encode(h) + "\n"
44
+ end
45
+
46
+ def iso8601_timestamps?
47
+ @iso8601_timestamps
48
+ end
49
+ end
@@ -0,0 +1,101 @@
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
+ # Creates a config given a yaml file of the following form:
14
+ #
15
+ # logging:
16
+ # level: <info, debug, etc>
17
+ # file: </path/to/logfile>
18
+ # syslog: <syslog name>
19
+ #
20
+ # @param [String] path Path to yaml config
21
+ # @param [Hash] overrides
22
+ #
23
+ # @return [Steno::Config]
24
+ def from_file(path, overrides = {})
25
+ h = YAML.load_file(path)
26
+ h = h["logging"] || {}
27
+ new(to_config_hash(h).merge(overrides))
28
+ end
29
+
30
+ def from_hash(hash)
31
+ new(to_config_hash(hash))
32
+ end
33
+
34
+ def to_config_hash(hash)
35
+ hash ||= {}
36
+ hash = symbolize_keys(hash)
37
+
38
+ level = hash[:level] || hash[:default_log_level]
39
+ opts = {
40
+ :sinks => [],
41
+ :default_log_level => level.nil? ? :info : level.to_sym
42
+ }
43
+
44
+ if hash[:iso8601_timestamps]
45
+ opts[:codec] = Steno::Codec::Json.new(:iso8601_timestamps => true)
46
+ end
47
+
48
+ if hash[:file]
49
+ max_retries = hash[:max_retries]
50
+ opts[:sinks] << Steno::Sink::IO.for_file(hash[:file], :max_retries => max_retries)
51
+ end
52
+
53
+ if Steno::Sink::WINDOWS
54
+ if hash[:eventlog]
55
+ Steno::Sink::Eventlog.instance.open(hash[:eventlog])
56
+ opts[:sinks] << Steno::Sink::Eventlog.instance
57
+ end
58
+ else
59
+ if hash[:syslog]
60
+ Steno::Sink::Syslog.instance.open(hash[:syslog])
61
+ opts[:sinks] << Steno::Sink::Syslog.instance
62
+ end
63
+ end
64
+
65
+ if hash[:fluentd]
66
+ opts[:sinks] << Steno::Sink::Fluentd.new(hash[:fluentd])
67
+ end
68
+
69
+ if opts[:sinks].empty?
70
+ opts[:sinks] << Steno::Sink::IO.new(STDOUT)
71
+ end
72
+
73
+ opts
74
+ end
75
+
76
+ def symbolize_keys(hash)
77
+ Hash[hash.each_pair.map { |k, v| [k.to_sym, v] }]
78
+ end
79
+ end
80
+
81
+ attr_reader :sinks
82
+ attr_reader :codec
83
+ attr_reader :context
84
+ attr_reader :default_log_level
85
+
86
+ def initialize(opts = {})
87
+ @sinks = opts[:sinks] || []
88
+ @codec = opts[:codec] || Steno::Codec::Json.new
89
+ @context = opts[:context] ||Steno::Context::Null.new
90
+
91
+ @sinks.each { |sink| sink.codec = @codec }
92
+
93
+ if opts[:default_log_level]
94
+ @default_log_level = opts[:default_log_level].to_sym
95
+ else
96
+ @default_log_level = :info
97
+ end
98
+ end
99
+
100
+ private_class_method :symbolize_keys
101
+ end