logfmt 0.0.9 → 0.1.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 460da978eb8342c65e74ebbd6cb5f2a37c71efb017c0f1d0a079a32c9e223ff4
4
- data.tar.gz: 4b3f4e10f500fa728c459e3b99c68089bb4e3ad85d5ec9c4872d55add8772cf0
3
+ metadata.gz: d8f2e9e3f3d271e1cad7120cb1fedc6a839050004a51ccd72a9e0ef019b242e6
4
+ data.tar.gz: 01035abc746fce52ac36c672bec2640a3c4d436f300b244d13b4e936b655030b
5
5
  SHA512:
6
- metadata.gz: b21d607d999d3275e2f3aedaf0bc11d1f4daa7a51141f2df90367f225c113162f255c457ee854ac78f11fd43a7a9e33e24ef65b72d734bf31c8270dd6e9d3709
7
- data.tar.gz: 5535f4b5ce7180c1e0c6263149da57ec8811b14146b92733f4b3f217f41f1fec5eb81bb49b26f62608471f9a9dcad86a0159ca78eec43886570e41b3e18999e8
6
+ metadata.gz: bb92737ee195acf40cfae4ae48b73895165568690984448e75d9bd4c3a3131f570721ebf0182778c959dc312182f6ebc9bcc0a6a26a877f09e24faa16f2377ff
7
+ data.tar.gz: 3df2ec4510aad12b18cbe6d1f2da2ffc7b8e3114bb3c66a4b74ae9d04965a5ba766a90db1b7510104295e0c133d36b2bff7190ac80bbbdfac85d770a4b1bb98d
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --color
2
2
  --format progress
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
5
+
6
+ ## \[Unreleased\]
7
+
8
+ ## [0.1.0.beta.1] 2022-10-21
9
+ ### Added
10
+ - Add `Logfmt::Logger` and `Logfmt::TaggedLogger`.
11
+ The later is distributed as its own gem, `logfmt-tagged_logger`, but lives in this repo.
12
+
13
+ ## [0.0.10] 2022-04-30
14
+ ### Changed
15
+ - Autoload the `Logfmt::Parser` when it's used, in preparation for the coming `Logfmt::Logger` and friends.
16
+ Alternatively you can eager-load it into memory: `require "logfmt/parser"`.
17
+
18
+ ### Added
19
+ - This CHANGELOG file.
data/Gemfile CHANGED
@@ -1,3 +1,4 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
- gemspec
3
+ gemspec name: "logfmt"
4
+ gemspec name: "logfmt-tagged_logger"
data/README.md CHANGED
@@ -1,7 +1,154 @@
1
1
  # Logfmt
2
2
 
3
- Parse log lines on the logfmt style:
3
+ Write and parse structured log lines in the [logfmt style][logfmt-blog].
4
4
 
5
- >> require "logfmt"
6
- >> Logfmt.parse('foo=bar a=14 baz="hello kitty" cool%story=bro f %^asdf')
7
- => {"foo"=>"bar", "a"=>14, "baz"=>"hello kitty", "cool%story"=>"bro", "f"=>true, "%^asdf"=>true}
5
+ ## Installation
6
+
7
+ Add this line to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "logfmt"
11
+ ```
12
+
13
+ And then install:
14
+
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself:
20
+
21
+ ```bash
22
+ $ gem install logfmt
23
+ ```
24
+
25
+ ### Versioning
26
+
27
+ This project adheres to [Semantic Versioning][semver].
28
+
29
+ ## Usage
30
+
31
+ `Logfmt` is composed to two parts: writing structured log lines in the `logfmt` style, and parsing `logfmt`-style log lines.
32
+
33
+ While writing and parsing `logfmt` are related, we've found that it's common to only need to do one or there other in a single application.
34
+ To support that usage, `Logfmt` leverages Ruby's `autoload` to lazily load the `Logfmt::Parser` or `Logfmt::Logger` (and associated code) into memory.
35
+ In the general case that looks something like:
36
+
37
+ ```ruby
38
+ require "logfmt"
39
+
40
+ Logfmt # This constant was already loaded, but neither Logfmt::Parser
41
+ # nor Logfmt::Logger constants are loaded. Yet.
42
+
43
+ Logfmt.parse("…")
44
+ # OR
45
+ Logfmt::Parser.parse("…")
46
+
47
+ # Either of the above will load the Logfmt::Parser constant.
48
+ # Similarly you can autoload the Logfmt::Logger via
49
+
50
+ Logfmt::Logger.new
51
+ ```
52
+
53
+ If you want to eagerly load the logger or parser, you can do that by requiring them directly
54
+
55
+ ### Parsing log lines
56
+
57
+ ```ruby
58
+ require "logfmt/parser"
59
+
60
+ Logfmt::Parser.parse('foo=bar a=14 baz="hello kitty" cool%story=bro f %^asdf')
61
+ #=> {"foo"=>"bar", "a"=>14, "baz"=>"hello kitty", "cool%story"=>"bro", "f"=>true, "%^asdf"=>true}
62
+ ```
63
+
64
+ ### Writing log lines
65
+
66
+ The `Logfmt::Logger` is built on the stdlib `::Logger` and adheres to its API.
67
+ The primary difference is that `Logfmt::Logger` defaults to a `logfmt`-style formatter.
68
+ Specifically, a `Logfmt::Logger::KeyValueFormatter`, which results in log lines something like this:
69
+
70
+ ```ruby
71
+ require "logfmt/logger"
72
+
73
+ logger = Logfmt::Logger.new($stdout)
74
+
75
+ logger.info(foo: "bar", a: 14, "baz" => "hello kitty", "cool%story" => "bro", f: true, "%^asdf" => true)
76
+ #=> time=2022-04-20T23:30:54.647403Z severity=INFO foo=bar a=14 baz="hello kitty" cool%story=bro f %^asdf
77
+
78
+ logger.debug("MADE IT HERE!")
79
+ #=> time=2022-04-20T23:33:44.912595Z severity=DEBUG msg="MADE IT HERE!"
80
+ ```
81
+
82
+ #### Tagged log lines
83
+
84
+ The `logfmt-tagged_logger` gem adds support for Rails-style [tagged logging][tagged-logger].
85
+ This gem adds a `Logfmt::TaggedLogger` which is built on `ActiveSupport::TaggedLogger`, but emits the tags in logfmt-style, as key/value pairs.
86
+ For example
87
+
88
+ ```ruby
89
+ logger = Logfmt::TaggedLogger.new($stdout)
90
+
91
+ logger.tagged(source: "api") do
92
+ logger.info(foo: "bar")
93
+ end
94
+
95
+ #=> time=2022-04-20T23:33:44.912595Z severity=info source=api foo=bar"
96
+ ```
97
+
98
+ You can also pass "bare" tags and they'll be collected and emitted under the `tags` key.
99
+
100
+ ```ruby
101
+ logger = Logfmt::TaggedLogger.new($stdout)
102
+
103
+ logger.tagged("API", "1.2.3.4") do
104
+ logger.info(foo: "bar")
105
+ end
106
+
107
+ #=> time=2022-04-20T23:33:44.912595Z severity=info tags="[API] [1.2.3.4]" foo=bar"
108
+ ```
109
+
110
+ It's likely more helpful and useful to use meaningful key/values for your tags, rather than bare tags.
111
+
112
+ #### Expected key/value transformations
113
+
114
+ When writing a log line with the `Logfmt::Logger::KeyValueFormatter` the keys and/or values will be transformed thusly:
115
+
116
+ * "Bare messages" (those with no key given when invoking the logger) will be wrapped in the `msg` key.
117
+
118
+ ```ruby
119
+ logger.info("here")
120
+ #=> time=2022-04-20T23:33:49.912997Z severity=INFO msg=here
121
+ ```
122
+
123
+ * Values, including bare messages, containing white space or control characters (spaces, tabs, newlines, emoji, etc…) will be wrapped in double quotes (`""`) and fully escaped.
124
+
125
+ ```ruby
126
+ logger.info("👻 Boo!")
127
+ #=> time=2022-04-20T23:33:35.912595Z severity=INFO msg="\u{1F47B} Boo!"
128
+
129
+ logger.info(number: 42, with_quotes: %{These "are" 'quotes', OK?})
130
+ #=> time=2022-04-20T23:33:36.412183Z severity=INFO number=42 with_quotes="These \"are\" 'quotes', OK?"
131
+ ```
132
+
133
+ * Floating point values are truncated to three digits.
134
+
135
+ * Time values are formatted as ISO8601 strings, with six digits sub-second precision.
136
+
137
+ * A value that is an Array is wrapped in square brackets, and then the above rules applied to each Array value.
138
+ This works well for arrays of simple values - like numbers, symbols, or simple strings.
139
+ But complex data structures will result in human mind-breaking escape sequences.
140
+ So don't do that.
141
+ Keep values simple.
142
+
143
+ ```ruby
144
+ logger.info(an_array: [1, "two", :three])
145
+ #=> time=2022-04-20T23:33:36.412183Z severity=INFO an_array="[1, two, three]"
146
+ ```
147
+
148
+ **NOTE**: it is **not** expected that log lines generated by `Logfmt` can be round-tripped by parsing the log line with `Logfmt`.
149
+ Specifically, this applies to Unicode and some control characters, as well as bare messages which will be wrapped in the `msg` key when writing.
150
+ Additionally, symbol keys will be parsed back into string keys.
151
+
152
+ [logfmt-blog]: https://brandur.org/logfmt "Structured log lines with key/value pairs"
153
+ [semver]: https://semver.org/spec/v2.0.0.html "Semantic Versioning 2.0.0"
154
+ [tagged-logger]: https://guides.rubyonrails.org/debugging_rails_applications.html#tagged-logging "Tagged Logging"
data/Rakefile CHANGED
@@ -1,6 +1,37 @@
1
- require 'bundler/gem_tasks'
2
- require 'rspec/core/rake_task'
1
+ # frozen_string_literal: true
3
2
 
3
+ require "bundler/gem_helper"
4
+ require "rspec/core/rake_task"
5
+
6
+ desc "Run all specs"
4
7
  RSpec::Core::RakeTask.new(:spec)
5
8
 
9
+ namespace "logfmt" do
10
+ Bundler::GemHelper.install_tasks name: "logfmt"
11
+ end
12
+
13
+ # Inspired by how dotenv/dotenv-rails handles mulitple Gems in a single repo
14
+ class LogFmtTaggedLoggerGemHelper < Bundler::GemHelper
15
+ def guard_already_tagged
16
+ # noop
17
+ end
18
+
19
+ def tag_version
20
+ # noop
21
+ end
22
+ end
23
+
24
+ namespace "logfmt-tagged_logger" do
25
+ LogFmtTaggedLoggerGemHelper.install_tasks name: "logfmt-tagged_logger"
26
+ end
27
+
28
+ desc "Build logfmt and logfmt-tagged_logger into the pkg directory"
29
+ task build: ["logfmt:build", "logfmt-tagged_logger:build"]
30
+
31
+ desc "Build and install logfmt and logfmt-tagged_logger into system gems"
32
+ task install: ["logfmt:install", "logfmt-tagged_logger:install"]
33
+
34
+ desc "Create tag, build, and push logfmt and logfmt-tagged_logger to rubygems.org"
35
+ task release: ["logfmt:release", "logfmt-tagged_logger:release"]
36
+
6
37
  task default: :spec
data/bench.rb CHANGED
@@ -1,13 +1,15 @@
1
- $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
2
 
3
- require 'benchmark'
4
- require 'logfmt'
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
4
+
5
+ require "benchmark"
6
+ require "logfmt"
5
7
 
6
8
  N = 1_000
7
9
  line = 'foo=bar a=14 baz="hello kitty" ƒ=2h3s cool%story=bro f %^asdf'
8
10
 
9
11
  Benchmark.bm(20) do |x|
10
- x.report('char-by-char') do
12
+ x.report("char-by-char") do
11
13
  N.times do
12
14
  Logfmt.parse(line)
13
15
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../logfmt"
4
+ require "logger"
5
+ require "time"
6
+
7
+ module Logfmt
8
+ class Logger < ::Logger
9
+ def initialize(*args, **kwargs)
10
+ super
11
+ @formatter ||= KeyValueFormatter.new
12
+ end
13
+
14
+ class KeyValueFormatter < ::Logger::Formatter
15
+ def call(severity, timestamp, progname, msg)
16
+ %(time=#{format_datetime(timestamp)} severity=#{severity.ljust(5)}#{format_progname(progname)} #{format_message(msg)}\n)
17
+ end
18
+
19
+ private
20
+
21
+ def format_datetime(time)
22
+ time.utc.iso8601(6)
23
+ end
24
+
25
+ def format_message(msg)
26
+ return unless msg
27
+
28
+ if msg.respond_to?(:to_hash)
29
+ pairs = msg.to_hash.map { |k, v| format_pair(k, v) }
30
+ pairs.compact.join(" ")
31
+ else
32
+ format_pair("msg", msg)
33
+ end
34
+ end
35
+
36
+ def format_pair(key, value)
37
+ return nil if value.nil?
38
+
39
+ # Return a bare key when the value is a `TrueClass`
40
+ return key if value == true
41
+
42
+ "#{key}=#{format_value(value)}"
43
+ end
44
+
45
+ def format_progname(progname)
46
+ return nil unless progname
47
+
48
+ # Format this pair like any other to ensure quoting, escaping, etc…,
49
+ # But we also need a leading space so we can interpolate the resulting
50
+ # key/value pair into our log line.
51
+ " #{format_pair(" progname", progname)}"
52
+ end
53
+
54
+ def format_value(value)
55
+ if value.is_a?(Float)
56
+ format("%.3f", value)
57
+ elsif value.is_a?(Time)
58
+ format_datetime(value)
59
+ elsif value.respond_to?(:to_ary)
60
+ format_value(
61
+ "[#{Array(value).map { |v| format_value(v) }.join(", ")}]"
62
+ )
63
+ else
64
+ # Interpolating due to a weird/subtle behaviour possible in #to_s.
65
+ # Namely, it's possible it doesn't actually return a String:
66
+ # https://github.com/ruby/spec/blob/3affe1e54fcd11918a242ad5d4a7ba895ee30c4c/language/string_spec.rb#L130-L141
67
+ value = "#{value}" # rubocop:disable Style/RedundantInterpolation
68
+ value = value.dump if value.match?(/[[:space:]]|[[:cntrl:]]/) # wrap in quotes and escape control characters
69
+ value
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
data/lib/logfmt/parser.rb CHANGED
@@ -1,112 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../logfmt"
4
+
1
5
  module Logfmt
2
- GARBAGE = 0
3
- KEY = 1
4
- EQUAL = 2
5
- IVALUE = 3
6
- QVALUE = 4
6
+ module Parser
7
+ GARBAGE = 0
8
+ KEY = 1
9
+ EQUAL = 2
10
+ IVALUE = 3
11
+ QVALUE = 4
7
12
 
8
- def self.numeric?(s)
9
- s.is_a?(Numeric) || s.to_s.match(/\A[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?\Z/)
10
- end
13
+ def self.numeric?(s)
14
+ s.is_a?(Numeric) || s.to_s.match?(/\A[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?\Z/)
15
+ end
11
16
 
12
- def self.integer?(s)
13
- s.is_a?(Integer) || s.to_s.match(/\A[-+]?[0-9]+\Z/)
14
- end
17
+ def self.integer?(s)
18
+ s.is_a?(Integer) || s.to_s.match?(/\A[-+]?[0-9]+\Z/)
19
+ end
15
20
 
16
- def self.parse(line)
17
- output = {}
18
- key, value = '', ''
19
- escaped = false
20
- state = GARBAGE
21
- i = 0
22
- line.each_char do |c|
23
- i += 1
24
- if state == GARBAGE
25
- if c > ' ' && c != '"' && c != '='
26
- key = c
27
- state = KEY
28
- end
29
- next
30
- end
31
- if state == KEY
32
- if c > ' ' && c != '"' && c != '='
33
- state = KEY
34
- key << c
35
- elsif c == '='
36
- output[key.strip] = true
37
- state = EQUAL
38
- else
39
- output[key.strip] = true
40
- state = GARBAGE
41
- end
42
- output[key.strip] = true if i >= line.length
43
- next
44
- end
45
- if state == EQUAL
46
- if c > ' ' && c != '"' && c != '='
47
- value = c
48
- state = IVALUE
49
- elsif c == '"'
50
- value = ''
51
- escaped = false
52
- state = QVALUE
53
- else
54
- state = GARBAGE
21
+ def self.parse(line)
22
+ output = {}
23
+ key, value = +"", +""
24
+ escaped = false
25
+ state = GARBAGE
26
+ i = 0
27
+ line.each_char do |c|
28
+ i += 1
29
+ if state == GARBAGE
30
+ if c > " " && c != '"' && c != "="
31
+ key = c
32
+ state = KEY
33
+ end
34
+ next
55
35
  end
56
- if i >= line.length
57
- if integer?(value)
58
- value = value.to_i
59
- elsif numeric?(value)
60
- fvalue = value.to_f
61
- value = fvalue if fvalue.finite?
36
+ if state == KEY
37
+ if c > " " && c != '"' && c != "="
38
+ state = KEY
39
+ key << c
40
+ elsif c == "="
41
+ output[key.strip] = true
42
+ state = EQUAL
43
+ else
44
+ output[key.strip] = true
45
+ state = GARBAGE
62
46
  end
63
- output[key.strip] = value || true
47
+ output[key.strip] = true if i >= line.length
48
+ next
64
49
  end
65
- next
66
- end
67
- if state == IVALUE
68
- if !(c > ' ' && c != '"')
69
- if integer?(value)
70
- value = value.to_i
71
- elsif numeric?(value)
72
- fvalue = value.to_f
73
- value = fvalue if fvalue.finite?
50
+ if state == EQUAL
51
+ if c > " " && c != '"' && c != "="
52
+ value = c
53
+ state = IVALUE
54
+ elsif c == '"'
55
+ value = +""
56
+ escaped = false
57
+ state = QVALUE
58
+ else
59
+ state = GARBAGE
60
+ end
61
+ if i >= line.length
62
+ if integer?(value)
63
+ value = value.to_i
64
+ elsif numeric?(value)
65
+ fvalue = value.to_f
66
+ value = fvalue if fvalue.finite?
67
+ end
68
+ output[key.strip] = value || true
74
69
  end
75
- output[key.strip] = value
76
- state = GARBAGE
77
- else
78
- value << c
70
+ next
79
71
  end
80
- if i >= line.length
81
- if integer?(value)
82
- value = value.to_i
83
- elsif numeric?(value)
84
- fvalue = value.to_f
85
- value = fvalue if fvalue.finite?
72
+ if state == IVALUE
73
+ if !(c > " " && c != '"')
74
+ if integer?(value)
75
+ value = value.to_i
76
+ elsif numeric?(value)
77
+ fvalue = value.to_f
78
+ value = fvalue if fvalue.finite?
79
+ end
80
+ output[key.strip] = value
81
+ state = GARBAGE
82
+ else
83
+ value << c
86
84
  end
87
- output[key.strip] = value
85
+ if i >= line.length
86
+ if integer?(value)
87
+ value = value.to_i
88
+ elsif numeric?(value)
89
+ fvalue = value.to_f
90
+ value = fvalue if fvalue.finite?
91
+ end
92
+ output[key.strip] = value
93
+ end
94
+ next
88
95
  end
89
- next
90
- end
91
- if state == QVALUE
92
- if c == '\\'
93
- escaped = true
94
- value << '\\'
95
- elsif c == '"'
96
- if escaped
96
+ if state == QVALUE
97
+ if c == "\\"
98
+ escaped = true
99
+ value << "\\"
100
+ elsif c == '"'
101
+ if escaped
102
+ escaped = false
103
+ value.chop! << c
104
+ next
105
+ end
106
+ output[key.strip] = value
107
+ state = GARBAGE
108
+ else
97
109
  escaped = false
98
- value.chop! << c
99
- next
110
+ value << c
100
111
  end
101
- output[key.strip] = value
102
- state = GARBAGE
103
- else
104
- escaped = false
105
- value << c
112
+ next
106
113
  end
107
- next
108
114
  end
115
+ output
109
116
  end
110
- output
111
117
  end
112
118
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Logfmt
2
- VERSION = '0.0.9'
4
+ VERSION = "0.1.0.beta.1"
3
5
  end
data/lib/logfmt.rb CHANGED
@@ -1,2 +1,12 @@
1
- require 'logfmt/version'
2
- require 'logfmt/parser'
1
+ # frozen_string_literal: true
2
+
3
+ require "logfmt/version"
4
+
5
+ module Logfmt
6
+ autoload(:Logger, "logfmt/logger")
7
+ autoload(:Parser, "logfmt/parser")
8
+
9
+ def self.parse(line)
10
+ const_get(:Parser).parse(line)
11
+ end
12
+ end
data/logfmt.gemspec CHANGED
@@ -1,22 +1,40 @@
1
- # -*- encoding: utf-8 -*-
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'logfmt/version'
1
+ # frozen_string_literal: true
5
2
 
6
- Gem::Specification.new do |gem|
7
- gem.name = 'logfmt'
8
- gem.version = Logfmt::VERSION
9
- gem.authors = ['Timothée Peignier']
10
- gem.email = ['timothee.peignier@tryphon.org']
11
- gem.description = %q{Parse log lines in the logfmt style.}
12
- gem.summary = %q{Parse logfmt messages.}
13
- gem.homepage = 'https://github.com/cyberdelia/logfmt-ruby'
14
- gem.license = 'MIT'
3
+ require_relative "lib/logfmt/version"
4
+ require "English"
15
5
 
16
- gem.files = `git ls-files`.split($/)
17
- gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
18
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
- gem.require_paths = ['lib']
20
- gem.add_development_dependency 'rspec', '~> 3.0'
21
- gem.add_development_dependency 'rake', '~> 10.3'
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "logfmt"
8
+ spec.version = Logfmt::VERSION
9
+ spec.authors = ["Timothée Peignier"]
10
+ spec.email = ["timothee.peignier@tryphon.org"]
11
+
12
+ spec.summary = "Write and parse logfmt messages."
13
+ spec.description = "Write and parse log lines in the logfmt style."
14
+ spec.homepage = "https://github.com/cyberdelia/logfmt-ruby"
15
+ spec.license = "MIT"
16
+ spec.required_ruby_version = ">= 2.5.0"
17
+
18
+ spec.metadata = {
19
+ "bug_tracker_uri" => "#{spec.homepage}/issues",
20
+ "changelog_uri" => "#{spec.homepage}/blog/master/CHANGELOG.md",
21
+ "documentation_uri" => spec.homepage,
22
+ "source_code_uri" => spec.homepage
23
+ }
24
+
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
27
+ .reject { |f|
28
+ (f == __FILE__) ||
29
+ f.match?(%r{\A(?:(?:bin|spec|features)/|\.(?:git|github))}) ||
30
+ f.match?(/tagged_logger/)
31
+ }
32
+ end
33
+ spec.bindir = "bin"
34
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
35
+ spec.require_paths = ["lib"]
36
+
37
+ spec.add_development_dependency "pry-byebug", "~> 3.9"
38
+ spec.add_development_dependency "rake", "~> 13.0"
39
+ spec.add_development_dependency "rspec", "~> 3.0"
22
40
  end
metadata CHANGED
@@ -1,68 +1,83 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logfmt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.1.0.beta.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Timothée Peignier
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-12-30 00:00:00.000000000 Z
11
+ date: 2022-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rspec
14
+ name: pry-byebug
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '3.0'
19
+ version: '3.9'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '3.0'
26
+ version: '3.9'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.3'
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
34
48
  type: :development
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
- version: '10.3'
41
- description: Parse log lines in the logfmt style.
54
+ version: '3.0'
55
+ description: Write and parse log lines in the logfmt style.
42
56
  email:
43
57
  - timothee.peignier@tryphon.org
44
58
  executables: []
45
59
  extensions: []
46
60
  extra_rdoc_files: []
47
61
  files:
48
- - ".github/workflows/gempush.yml"
49
- - ".github/workflows/ruby.yml"
50
- - ".gitignore"
51
62
  - ".rspec"
63
+ - CHANGELOG.md
52
64
  - Gemfile
53
65
  - README.md
54
66
  - Rakefile
55
67
  - bench.rb
56
68
  - lib/logfmt.rb
69
+ - lib/logfmt/logger.rb
57
70
  - lib/logfmt/parser.rb
58
71
  - lib/logfmt/version.rb
59
72
  - logfmt.gemspec
60
- - spec/logfmt/parser_spec.rb
61
- - spec/spec_helper.rb
62
73
  homepage: https://github.com/cyberdelia/logfmt-ruby
63
74
  licenses:
64
75
  - MIT
65
- metadata: {}
76
+ metadata:
77
+ bug_tracker_uri: https://github.com/cyberdelia/logfmt-ruby/issues
78
+ changelog_uri: https://github.com/cyberdelia/logfmt-ruby/blog/master/CHANGELOG.md
79
+ documentation_uri: https://github.com/cyberdelia/logfmt-ruby
80
+ source_code_uri: https://github.com/cyberdelia/logfmt-ruby
66
81
  post_install_message:
67
82
  rdoc_options: []
68
83
  require_paths:
@@ -71,17 +86,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
71
86
  requirements:
72
87
  - - ">="
73
88
  - !ruby/object:Gem::Version
74
- version: '0'
89
+ version: 2.5.0
75
90
  required_rubygems_version: !ruby/object:Gem::Requirement
76
91
  requirements:
77
- - - ">="
92
+ - - ">"
78
93
  - !ruby/object:Gem::Version
79
- version: '0'
94
+ version: 1.3.1
80
95
  requirements: []
81
- rubygems_version: 3.0.3
96
+ rubygems_version: 3.3.7
82
97
  signing_key:
83
98
  specification_version: 4
84
- summary: Parse logfmt messages.
85
- test_files:
86
- - spec/logfmt/parser_spec.rb
87
- - spec/spec_helper.rb
99
+ summary: Write and parse logfmt messages.
100
+ test_files: []
@@ -1,24 +0,0 @@
1
- name: Publish
2
- on:
3
- release:
4
- types: [created]
5
- jobs:
6
- build:
7
- name: Build + Publish
8
- runs-on: ubuntu-latest
9
- steps:
10
- - uses: actions/checkout@master
11
- - name: Set up Ruby 2.6
12
- uses: actions/setup-ruby@v1
13
- with:
14
- version: 2.6.x
15
- - name: Publish to RubyGems
16
- run: |
17
- mkdir -p $HOME/.gem
18
- touch $HOME/.gem/credentials
19
- chmod 0600 $HOME/.gem/credentials
20
- printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
21
- gem build *.gemspec
22
- gem push *.gem
23
- env:
24
- GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}}
@@ -1,19 +0,0 @@
1
- name: CI
2
- on: [push]
3
- jobs:
4
- build:
5
- runs-on: ubuntu-latest
6
- strategy:
7
- matrix:
8
- ruby: [ '2.4', '2.5', '2.6' ]
9
- steps:
10
- - uses: actions/checkout@v1
11
- - name: Set up Ruby
12
- uses: actions/setup-ruby@v1
13
- with:
14
- ruby-version: ${{ matrix.ruby }}
15
- - name: Run tests
16
- run: |
17
- gem install bundler
18
- bundle install --jobs 4 --retry 3
19
- bundle exec rspec spec
data/.gitignore DELETED
@@ -1,19 +0,0 @@
1
- *.gem
2
- *.rbc
3
- .bundle
4
- .config
5
- .yardoc
6
- Gemfile.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
- doc/
11
- lib/bundler/man
12
- pkg
13
- rdoc
14
- spec/reports
15
- test/tmp
16
- test/version_tmp
17
- tmp
18
- .ruby-version
19
- .tags
@@ -1,159 +0,0 @@
1
- # encoding: utf-8
2
- require 'spec_helper'
3
- require 'logfmt/parser'
4
-
5
- describe Logfmt do
6
- it 'parse empty log line' do
7
- data = Logfmt.parse('')
8
- expect(data).to eq({})
9
- end
10
-
11
- it 'parse whitespace only log line' do
12
- data = Logfmt.parse("\t")
13
- expect(data).to eq({})
14
- end
15
-
16
- it 'parse key without value' do
17
- data = Logfmt.parse('key')
18
- expect(data).to eq('key' => true)
19
- end
20
-
21
- it 'parse key without value and whitespace' do
22
- data = Logfmt.parse(' key ')
23
- expect(data).to eq('key' => true)
24
- end
25
-
26
- it 'parse multiple single keys' do
27
- data = Logfmt.parse('key1 key2')
28
- expect(data).to eq('key1' => true, 'key2' => true)
29
- end
30
-
31
- it 'parse unquoted value' do
32
- data = Logfmt.parse('key=value')
33
- expect(data).to eq('key' => 'value')
34
- end
35
-
36
- it 'parse pairs' do
37
- data = Logfmt.parse('key1=value1 key2=value2')
38
- expect(data).to eq('key1' => 'value1', 'key2' => 'value2')
39
- end
40
-
41
- it 'parse mixed single/non-single pairs' do
42
- data = Logfmt.parse('key1=value1 key2')
43
- expect(data).to eq('key1' => 'value1', 'key2' => true)
44
- end
45
-
46
- it 'parse mixed pairs whatever the order' do
47
- data = Logfmt.parse('key1 key2=value2')
48
- expect(data).to eq('key1' => true, 'key2' => 'value2')
49
- end
50
-
51
- it 'parse quoted value' do
52
- data = Logfmt.parse('key="quoted value"')
53
- expect(data).to eq('key' => 'quoted value')
54
- end
55
-
56
- it 'parse escaped quote value ' do
57
- data = Logfmt.parse('key="quoted \" value" r="esc\t"')
58
- expect(data).to eq('key' => 'quoted " value', 'r' => 'esc\t')
59
- end
60
-
61
- it 'parse mixed pairs' do
62
- data = Logfmt.parse('key1="quoted \" value" key2 key3=value3')
63
- expect(data).to eq('key1' => 'quoted " value', 'key2' => true, 'key3' => 'value3')
64
- end
65
-
66
- it 'parse mixed characters pairs' do
67
- data = Logfmt.parse('foo=bar a=14 baz="hello kitty" ƒ=2h3s cool%story=bro f %^asdf')
68
- expect(data).to eq('foo' => 'bar', 'a' => 14, 'baz' => 'hello kitty',
69
- 'ƒ' => '2h3s', 'cool%story' => 'bro', 'f' => true, '%^asdf' => true)
70
- end
71
-
72
- it 'parse pair with empty quote' do
73
- data = Logfmt.parse('key=""')
74
- expect(data).to eq('key' => '')
75
- end
76
-
77
- # Currently, the value comes back as "true", which could mess up stats
78
- # Really, only "true" should come back as "true"
79
- # it 'parse 1 as integer type' do
80
- # data = Logfmt.parse('key=1')
81
- # expect(data['key'].class).to eq(Fixnum)
82
- # end
83
-
84
- it 'parse positive integer as integer type' do
85
- data = Logfmt.parse('key=234')
86
- expect(data['key']).to eq(234)
87
- expect(data['key'].class).to eq(Fixnum)
88
- end
89
-
90
- it 'parse negative integer as integer type' do
91
- data = Logfmt.parse('key=-3428')
92
- expect(data['key']).to eq(-3428)
93
- expect(data['key'].class).to eq(Fixnum)
94
- end
95
-
96
- it 'parse positive float as float type' do
97
- data = Logfmt.parse('key=3.342')
98
- expect(data['key']).to eq(3.342)
99
- expect(data['key'].class).to eq(Float)
100
- end
101
-
102
- it 'parse negative float as float type' do
103
- data = Logfmt.parse('key=-0.9934')
104
- expect(data['key']).to eq(-0.9934)
105
- expect(data['key'].class).to eq(Float)
106
- end
107
-
108
- it 'parse exponential float as float type' do
109
- data = Logfmt.parse('key=2.342342342342344e+18')
110
- expect(data['key']).to eq(2.342342342342344e+18)
111
- expect(data['key'].class).to eq(Float)
112
- end
113
-
114
- it 'parse long digit string with embedded e as string' do
115
- data = Logfmt.parse('key=2342342342342344e1818')
116
- expect(data['key'].class).to eq(String)
117
- end
118
-
119
- it 'parse quoted integer as string type' do
120
- data = Logfmt.parse('key="234"')
121
- expect(data['key'].class).to eq(String)
122
- end
123
-
124
- it 'parse quoted float as string type' do
125
- data = Logfmt.parse('key="3.14"')
126
- expect(data['key'].class).to eq(String)
127
- end
128
-
129
- it 'parse IP address as string type' do
130
- data = Logfmt.parse('key=10.10.10.1')
131
- expect(data['key'].class).to eq(String)
132
- end
133
-
134
- it 'parse last as integer type' do
135
- data = Logfmt.parse('key1=4 key2=9')
136
- expect(data['key1']).to eq(4)
137
- expect(data['key2']).to eq(9)
138
- end
139
-
140
- it 'parse string containing quotes' do
141
- data = Logfmt.parse('key1="{\"msg\": \"hello\tworld\"}"')
142
- expect(data['key1']).to eq('{"msg": "hello\tworld"}')
143
- end
144
-
145
- it 'parse value containing equal sign' do
146
- query = 'position=44.80450799126121%2C33.58320759981871&uid=1'
147
- data = Logfmt.parse("method=GET query=#{query} status=200")
148
- expect(data).to eq(
149
- 'method' => 'GET',
150
- 'query' => query,
151
- 'status' => 200
152
- )
153
- end
154
-
155
- it 'parses integers correctly' do
156
- data = Logfmt.parse('key=111 ')
157
- expect(data['key']).to eq(111)
158
- end
159
- end
data/spec/spec_helper.rb DELETED
@@ -1,16 +0,0 @@
1
- # This file was generated by the `rspec --init` command. Conventionally, all
2
- # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
- # Require this file using `require "spec_helper"` to ensure that it is only
4
- # loaded once.
5
- #
6
- # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
- RSpec.configure do |config|
8
- config.run_all_when_everything_filtered = true
9
- config.filter_run :focus
10
-
11
- # Run specs in random order to surface order dependencies. If you find an
12
- # order dependency and want to debug it, you can fix the order by providing
13
- # the seed, which is printed after each run.
14
- # --seed 1234
15
- config.order = 'random'
16
- end