├── .travis.yml ├── Gemfile ├── spec ├── spec_helper.rb └── ltsv_log_formatter_spec.rb ├── .gitignore ├── CHANGELOG.md ├── Rakefile ├── ltsv_log_formatter.gemspec ├── LICENSE.txt ├── lib └── ltsv_log_formatter.rb └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.0.0 4 | - 2.1 5 | - 2.2 6 | gemfile: 7 | - Gemfile 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'rspec' 6 | gem 'timecop' 7 | gem 'rake' 8 | gem 'pry' 9 | gem 'pry-nav' 10 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.setup(:default, :test) 3 | Bundler.require(:default, :test) 4 | 5 | $TESTING=true 6 | $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 | example/log 19 | 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 (2017/01/07) 2 | 3 | * Just bump up to 1.0.0 to use semantic versioning, yay! 4 | 5 | # 0.0.2 (2017/01/07) 6 | 7 | Enhancements: 8 | 9 | * Escape "\t" character to "\\t" 10 | 11 | # 0.0.1 (2015/04/23) 12 | 13 | First version 14 | 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | task :default => :test 4 | 5 | task :test do 6 | require 'rspec/core' 7 | require 'rspec/core/rake_task' 8 | RSpec::Core::RakeTask.new(:test) do |spec| 9 | spec.pattern = FileList['spec/**/*_spec.rb'] 10 | end 11 | end 12 | 13 | desc 'Open an irb session preloaded with the gem library' 14 | task :console do 15 | sh 'irb -rubygems -I lib -r strftime_logger' 16 | end 17 | task :c => :console 18 | -------------------------------------------------------------------------------- /ltsv_log_formatter.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = "ltsv_log_formatter" 7 | gem.version = "1.0.0" 8 | gem.authors = ["Naotoshi Seo"] 9 | gem.email = ["sonots@gmail.com"] 10 | gem.description = %q{A logger formatter to output log in LTSV format} 11 | gem.summary = %q{A logger formatter to output log in LTSV format} 12 | gem.homepage = "https://github.com/sonots/ltsv_log_formatter" 13 | 14 | gem.files = `git ls-files`.split($/) 15 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 16 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 17 | gem.require_paths = ["lib"] 18 | end 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Naotoshi Seo 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. 23 | -------------------------------------------------------------------------------- /lib/ltsv_log_formatter.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | class LtsvLogFormatter 4 | # for tests 5 | attr_reader :opts 6 | 7 | # @param [Hash] opts 8 | # @option opts [String] time_key (default: time) 9 | # @option opts [String] level_key (default: level) 10 | # @option opts [String] message_key (default: message) 11 | def initialize(opts={}) 12 | @opts = opts.map {|k, v| [k.to_sym, v] }.to_h # symbolize_keys 13 | @opts[:time_key] = :time unless @opts.has_key?(:time_key) 14 | @opts[:level_key] = :level unless @opts.has_key?(:level_key) 15 | @opts[:message_key] ||= :message 16 | end 17 | 18 | def call(severity, time, progname, msg) 19 | "#{format_time(time)}#{format_severity(severity)}#{format_message(msg)}\n" 20 | end 21 | 22 | private 23 | def format_time(time) 24 | "#{@opts[:time_key]}:#{time.iso8601}\t" if @opts[:time_key] 25 | end 26 | 27 | def format_severity(severity) 28 | "#{@opts[:level_key]}:#{severity}\t" if @opts[:level_key] 29 | end 30 | 31 | LF = "\n".freeze 32 | TAB = "\t".freeze 33 | ESCAPED_LF = "\\n".freeze 34 | ESCAPED_TAB = "\\t".freeze 35 | ESCAPE_TARGET = /[#{LF}#{TAB}]/ 36 | 37 | def format_message(msg) 38 | unless msg.is_a?(Hash) 39 | msg = { @opts[:message_key] => msg } 40 | end 41 | msg.map {|k, v| "#{k}:#{v.to_s.gsub(ESCAPE_TARGET, LF => ESCAPED_LF, TAB => ESCAPED_TAB)}" }.join(TAB) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LtsvLogFormatter 2 | 3 | A ruby logger formatter to output log in LTSV format 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | gem 'ltsv_log_formatter' 10 | 11 | And then execute: 12 | 13 | $ bundle 14 | 15 | ## How to use 16 | 17 | ```ruby 18 | require 'logger' 19 | require 'ltsv_log_formatter' 20 | 21 | logger = Logger.new(STDOUT) 22 | logger.formatter = LtsvLogFormatter.new 23 | ``` 24 | 25 | ## Rails 26 | 27 | Configure at `config/application.rb` 28 | 29 | ```ruby 30 | config.logger.formatter = LtsvLogFormatter.new 31 | ``` 32 | 33 | ## Example 34 | 35 | Passing a hash parameter: 36 | 37 | ``` 38 | irb> logger.info({foo: "foo", bar: "bar"}) 39 | time:20150423T00:00:00+09:00\tlevel:INFO\tfoo:foo\tbar:bar 40 | ``` 41 | 42 | Passing a string parameter: `message` key is used as default 43 | 44 | ```ruby 45 | irb> logger.info("foo") 46 | time:20150423T00:00:00+09:00\tlevel:INFO\tmessage:foo 47 | ``` 48 | 49 | NOTE1: Notice that the line feed character `\n` is converted into `\\n` because LTSV format does not allow to break lines 50 | 51 | ```ruby 52 | irb> logger.info("foo\nbar") 53 | time:20150423T00:00:00+09:00\tlevel:INFO\tmessage:foo\\nbar 54 | ``` 55 | 56 | NOTE2: Notice that the tab character `\t` is converted into `\\t` because message might have a string as "\t:" 57 | 58 | ```ruby 59 | irb> logger.info("foo\tbar:baz") 60 | time:20150423T00:00:00+09:00\tlevel:INFO\tmessage:foo\\tbar:baz 61 | ``` 62 | 63 | ## Options 64 | 65 | * time_key 66 | * Change the key name of the time field. Set `nil` to remove. Default: time 67 | * level_key 68 | * Change the kay name of the level field. Set `nil` to remove. Default: level 69 | * message_key 70 | * Change the kay name for the string (not hash) message. Default: message 71 | 72 | Example) 73 | 74 | ```ruby 75 | logger.formatter = LtsvLogFormatter.new(time_key: "log_time", level_key: nil) 76 | ``` 77 | 78 | ## ChangeLog 79 | 80 | See [CHANGELOG.md](CHANGELOG.md) for details. 81 | 82 | ## Contributing 83 | 84 | 1. Fork it 85 | 2. Create your feature branch (`git checkout -b my-new-feature`) 86 | 3. Commit your changes (`git commit -am 'Add some feature'`) 87 | 4. Push to the branch (`git push origin my-new-feature`) 88 | 5. Create new [Pull Request](../../pull/new/master) 89 | 90 | ## Copyright 91 | 92 | See [LICENSE.txt](LICENSE.txt) for details. 93 | -------------------------------------------------------------------------------- /spec/ltsv_log_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | require 'ltsv_log_formatter' 3 | require 'fileutils' 4 | require 'logger' 5 | 6 | describe LtsvLogFormatter do 7 | let(:logger) do 8 | Logger.new("#{log_dir}/test.log").tap {|logger| 9 | logger.formatter = LtsvLogFormatter.new(opts) 10 | } 11 | end 12 | let(:opts) { {} } 13 | let(:log_dir) { "#{File.dirname(__FILE__)}/log" } 14 | let(:now) { Time.now.iso8601 } 15 | 16 | before do 17 | FileUtils.mkdir_p log_dir 18 | Timecop.freeze(Time.now) 19 | end 20 | 21 | after do 22 | FileUtils.rm_rf log_dir 23 | Timecop.return 24 | end 25 | 26 | def gets 27 | File.open("#{log_dir}/test.log") do |f| 28 | f.gets # drop the `# Logfile created on ...` line 29 | return f.gets 30 | end 31 | end 32 | 33 | describe 'options' do 34 | context '#initialize' do 35 | it 'with default' do 36 | formatter = LtsvLogFormatter.new 37 | expect(formatter.opts[:time_key]).to eql(:time) 38 | expect(formatter.opts[:level_key]).to eql(:level) 39 | expect(formatter.opts[:message_key]).to eql(:message) 40 | end 41 | 42 | it 'with time_key' do 43 | formatter = LtsvLogFormatter.new("time_key" => "foo") 44 | expect(formatter.opts[:time_key]).to eql("foo") 45 | end 46 | 47 | it 'with level_key' do 48 | formatter = LtsvLogFormatter.new("level_key" => "foo") 49 | expect(formatter.opts[:level_key]).to eql("foo") 50 | end 51 | 52 | it 'with message_key' do 53 | formatter = LtsvLogFormatter.new("message_key" => "foo") 54 | expect(formatter.opts[:message_key]).to eql("foo") 55 | end 56 | end 57 | 58 | context 'with time_key nil' do 59 | let(:opts) { {time_key: nil} } 60 | 61 | it 'should not output a time field' do 62 | logger.info("test") 63 | expect(gets).to eq "level:INFO\tmessage:test\n" 64 | end 65 | end 66 | 67 | context 'with level_key nil' do 68 | let(:opts) { {level_key: nil} } 69 | 70 | it 'should not output a level field' do 71 | logger.info("test") 72 | expect(gets).to eq "time:#{now}\tmessage:test\n" 73 | end 74 | end 75 | 76 | context 'with message_key nil' do 77 | let(:opts) { {message_key: nil} } 78 | 79 | it 'should have no effect' do 80 | logger.info("test") 81 | expect(gets).to eq "time:#{now}\tlevel:INFO\tmessage:test\n" 82 | end 83 | end 84 | end 85 | 86 | describe 'string param' do 87 | context 'with default' do 88 | it do 89 | logger.info("test") 90 | expect(gets).to eq "time:#{now}\tlevel:INFO\tmessage:test\n" 91 | end 92 | end 93 | 94 | context 'with message_key' do 95 | let(:opts) { {message_key: "foo"} } 96 | 97 | it do 98 | logger.info("test") 99 | expect(gets).to eq "time:#{now}\tlevel:INFO\tfoo:test\n" 100 | end 101 | end 102 | 103 | it 'with line feed' do 104 | logger.info("foo\nbar") 105 | expect(gets).to eq "time:#{now}\tlevel:INFO\tmessage:foo\\nbar\n" 106 | end 107 | end 108 | 109 | describe 'hash param' do 110 | context 'with default' do 111 | it do 112 | logger.info(foo: "bar") 113 | expect(gets).to eq "time:#{now}\tlevel:INFO\tfoo:bar\n" 114 | end 115 | end 116 | 117 | it 'with line feed' do 118 | logger.info(foo: "foo\nbar") 119 | expect(gets).to eq "time:#{now}\tlevel:INFO\tfoo:foo\\nbar\n" 120 | end 121 | end 122 | 123 | describe 'block param' do 124 | context 'with string' do 125 | it do 126 | logger.info { "test" } 127 | expect(gets).to eq "time:#{now}\tlevel:INFO\tmessage:test\n" 128 | end 129 | end 130 | 131 | context 'with hash' do 132 | it do 133 | logger.info { {foo: "bar"} } 134 | expect(gets).to eq "time:#{now}\tlevel:INFO\tfoo:bar\n" 135 | end 136 | end 137 | end 138 | 139 | describe 'escape LF and TAB' do 140 | context 'with LF' do 141 | it do 142 | logger.info(foo: "bar\nbar") 143 | expect(gets).to eq "time:#{now}\tlevel:INFO\tfoo:bar\\nbar\n" 144 | end 145 | end 146 | 147 | context 'with TAB' do 148 | it do 149 | logger.info(foo: "bar\tbar") 150 | expect(gets).to eq "time:#{now}\tlevel:INFO\tfoo:bar\\tbar\n" 151 | end 152 | end 153 | end 154 | end 155 | --------------------------------------------------------------------------------