lumberjack 1.0.13 → 1.2.8
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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +129 -0
- data/{MIT_LICENSE → MIT_LICENSE.txt} +0 -0
- data/README.md +142 -23
- data/VERSION +1 -1
- data/lib/lumberjack.rb +70 -22
- data/lib/lumberjack/context.rb +35 -0
- data/lib/lumberjack/device.rb +22 -8
- data/lib/lumberjack/device/date_rolling_log_file.rb +9 -9
- data/lib/lumberjack/device/log_file.rb +14 -3
- data/lib/lumberjack/device/multi.rb +46 -0
- data/lib/lumberjack/device/null.rb +1 -3
- data/lib/lumberjack/device/rolling_log_file.rb +45 -21
- data/lib/lumberjack/device/size_rolling_log_file.rb +10 -10
- data/lib/lumberjack/device/writer.rb +92 -61
- data/lib/lumberjack/formatter.rb +97 -28
- data/lib/lumberjack/formatter/date_time_formatter.rb +25 -0
- data/lib/lumberjack/formatter/exception_formatter.rb +25 -2
- data/lib/lumberjack/formatter/id_formatter.rb +23 -0
- data/lib/lumberjack/formatter/object_formatter.rb +12 -0
- data/lib/lumberjack/formatter/pretty_print_formatter.rb +4 -4
- data/lib/lumberjack/formatter/string_formatter.rb +1 -1
- data/lib/lumberjack/formatter/strip_formatter.rb +12 -0
- data/lib/lumberjack/formatter/structured_formatter.rb +63 -0
- data/lib/lumberjack/log_entry.rb +44 -16
- data/lib/lumberjack/logger.rb +275 -69
- data/lib/lumberjack/rack.rb +3 -2
- data/lib/lumberjack/rack/context.rb +18 -0
- data/lib/lumberjack/rack/request_id.rb +4 -4
- data/lib/lumberjack/rack/unit_of_work.rb +1 -1
- data/lib/lumberjack/severity.rb +11 -10
- data/lib/lumberjack/tag_formatter.rb +96 -0
- data/lib/lumberjack/tagged_logger_support.rb +66 -0
- data/lib/lumberjack/tagged_logging.rb +29 -0
- data/lib/lumberjack/tags.rb +42 -0
- data/lib/lumberjack/template.rb +81 -33
- data/lumberjack.gemspec +31 -0
- metadata +26 -53
- data/Rakefile +0 -40
- data/spec/device/date_rolling_log_file_spec.rb +0 -73
- data/spec/device/log_file_spec.rb +0 -48
- data/spec/device/null_spec.rb +0 -12
- data/spec/device/rolling_log_file_spec.rb +0 -151
- data/spec/device/size_rolling_log_file_spec.rb +0 -58
- data/spec/device/writer_spec.rb +0 -118
- data/spec/formatter/exception_formatter_spec.rb +0 -20
- data/spec/formatter/inspect_formatter_spec.rb +0 -13
- data/spec/formatter/pretty_print_formatter_spec.rb +0 -14
- data/spec/formatter/string_formatter_spec.rb +0 -12
- data/spec/formatter_spec.rb +0 -45
- data/spec/log_entry_spec.rb +0 -69
- data/spec/logger_spec.rb +0 -411
- data/spec/lumberjack_spec.rb +0 -29
- data/spec/rack/request_id_spec.rb +0 -48
- data/spec/rack/unit_of_work_spec.rb +0 -26
- data/spec/severity_spec.rb +0 -23
- data/spec/spec_helper.rb +0 -32
- data/spec/template_spec.rb +0 -34
data/lib/lumberjack/rack.rb
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
|
3
3
|
module Lumberjack
|
4
4
|
module Rack
|
5
|
-
|
6
|
-
|
5
|
+
require_relative "rack/unit_of_work"
|
6
|
+
require_relative "rack/request_id"
|
7
|
+
require_relative "rack/context"
|
7
8
|
end
|
8
9
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literals: true
|
2
|
+
|
3
|
+
module Lumberjack
|
4
|
+
module Rack
|
5
|
+
# Middleware to create a global context for Lumberjack for the scope of a rack request.
|
6
|
+
class Context
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
Lumberjack.context do
|
13
|
+
@app.call(env)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -6,17 +6,17 @@ module Lumberjack
|
|
6
6
|
# The format is expected to be a random UUID and only the first chunk is used for terseness
|
7
7
|
# if the abbreviated argument is true.
|
8
8
|
class RequestId
|
9
|
-
REQUEST_ID = "action_dispatch.request_id"
|
10
|
-
|
9
|
+
REQUEST_ID = "action_dispatch.request_id"
|
10
|
+
|
11
11
|
def initialize(app, abbreviated = false)
|
12
12
|
@app = app
|
13
13
|
@abbreviated = abbreviated
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
def call(env)
|
17
17
|
request_id = env[REQUEST_ID]
|
18
18
|
if request_id && @abbreviated
|
19
|
-
request_id = request_id.split(
|
19
|
+
request_id = request_id.split("-", 2).first
|
20
20
|
end
|
21
21
|
Lumberjack.unit_of_work(request_id) do
|
22
22
|
@app.call(env)
|
data/lib/lumberjack/severity.rb
CHANGED
@@ -3,20 +3,21 @@
|
|
3
3
|
module Lumberjack
|
4
4
|
# The standard severity levels for logging messages.
|
5
5
|
module Severity
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
WARN =
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
6
|
+
# Backward compatibilty with 1.0 API
|
7
|
+
DEBUG = ::Logger::Severity::DEBUG
|
8
|
+
INFO = ::Logger::Severity::INFO
|
9
|
+
WARN = ::Logger::Severity::WARN
|
10
|
+
ERROR = ::Logger::Severity::ERROR
|
11
|
+
FATAL = ::Logger::Severity::FATAL
|
12
|
+
UNKNOWN = ::Logger::Severity::UNKNOWN
|
13
|
+
|
14
|
+
SEVERITY_LABELS = %w[DEBUG INFO WARN ERROR FATAL UNKNOWN].freeze
|
15
|
+
|
15
16
|
class << self
|
16
17
|
def level_to_label(severity)
|
17
18
|
SEVERITY_LABELS[severity] || SEVERITY_LABELS.last
|
18
19
|
end
|
19
|
-
|
20
|
+
|
20
21
|
def label_to_level(label)
|
21
22
|
SEVERITY_LABELS.index(label.to_s.upcase) || UNKNOWN
|
22
23
|
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lumberjack
|
4
|
+
# Class for formatting tags. You can register a default formatter and tag
|
5
|
+
# name specific formatters. Formatters can be either `Lumberjack::Formatter`
|
6
|
+
# objects or any object that responds to `call`.
|
7
|
+
#
|
8
|
+
# tag_formatter = Lumberjack::TagFormatter.new.default(Lumberjack::Formatter.new)
|
9
|
+
# tag_formatter.add(["password", "email"]) { |value| "***" }
|
10
|
+
# tag_formatter.add("finished_at", Lumberjack::Formatter::DateTimeFormatter.new("%Y-%m-%dT%H:%m:%S%z"))
|
11
|
+
class TagFormatter
|
12
|
+
def initialize
|
13
|
+
@formatters = {}
|
14
|
+
@default_formatter = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
# Add a default formatter applied to all tag values. This can either be a Lumberjack::Formatter
|
18
|
+
# or an object that responds to `call` or a block.
|
19
|
+
def default(formatter = nil, &block)
|
20
|
+
formatter ||= block
|
21
|
+
formatter = dereference_formatter(formatter)
|
22
|
+
@default_formatter = formatter
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
# Remove the default formatter.
|
27
|
+
def remove_default
|
28
|
+
@default_formatter = nil
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
# Add a formatter for specific tag names. This can either be a Lumberjack::Formatter
|
33
|
+
# or an object that responds to `call` or a block. The default formatter will not be
|
34
|
+
# applied.
|
35
|
+
def add(names, formatter = nil, &block)
|
36
|
+
formatter ||= block
|
37
|
+
formatter = dereference_formatter(formatter)
|
38
|
+
if formatter.nil?
|
39
|
+
remove(key)
|
40
|
+
else
|
41
|
+
Array(names).each do |name|
|
42
|
+
@formatters[name.to_s] = formatter
|
43
|
+
end
|
44
|
+
end
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
# Remove formatters for specific tag names. The default formatter will still be applied.
|
49
|
+
def remove(names)
|
50
|
+
Array(names).each do |name|
|
51
|
+
@formatters.delete(name.to_s)
|
52
|
+
end
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# Remove all formatters.
|
57
|
+
def clear
|
58
|
+
@default_formatter = nil
|
59
|
+
@formatters.clear
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
# Format a hash of tags using the formatters
|
64
|
+
def format(tags)
|
65
|
+
return nil if tags.nil?
|
66
|
+
if @default_formatter.nil? && (@formatters.empty? || (@formatters.keys & tags.keys).empty?)
|
67
|
+
tags
|
68
|
+
else
|
69
|
+
formatted = {}
|
70
|
+
tags.each do |name, value|
|
71
|
+
formatter = (@formatters[name.to_s] || @default_formatter)
|
72
|
+
if formatter.is_a?(Lumberjack::Formatter)
|
73
|
+
value = formatter.format(value)
|
74
|
+
elsif formatter.respond_to?(:call)
|
75
|
+
value = formatter.call(value)
|
76
|
+
end
|
77
|
+
formatted[name.to_s] = value
|
78
|
+
end
|
79
|
+
formatted
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def dereference_formatter(formatter)
|
86
|
+
if formatter.is_a?(TaggedLoggerSupport::Formatter)
|
87
|
+
formatter.__formatter
|
88
|
+
elsif formatter.is_a?(Symbol)
|
89
|
+
formatter_class_name = "#{formatter.to_s.gsub(/(^|_)([a-z])/) { |m| $~[2].upcase }}Formatter"
|
90
|
+
Formatter.const_get(formatter_class_name).new
|
91
|
+
else
|
92
|
+
formatter
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "delegate"
|
4
|
+
require "forwardable"
|
5
|
+
|
6
|
+
module Lumberjack
|
7
|
+
# Methods to make Lumberjack::Logger API compatible with ActiveSupport::TaggedLogger.
|
8
|
+
module TaggedLoggerSupport
|
9
|
+
class Formatter < DelegateClass(Lumberjack::Formatter)
|
10
|
+
extend Forwardable
|
11
|
+
def_delegators :@logger, :tagged, :push_tags, :pop_tags, :clear_tags!
|
12
|
+
|
13
|
+
def initialize(formatter:, logger:)
|
14
|
+
@logger = logger
|
15
|
+
@formatter = formatter
|
16
|
+
super(formatter)
|
17
|
+
end
|
18
|
+
|
19
|
+
def current_tags
|
20
|
+
tags = @logger.instance_variable_get(:@tags)
|
21
|
+
if tags.is_a?(Hash)
|
22
|
+
Array(tags["tagged"])
|
23
|
+
else
|
24
|
+
[]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def tags_text
|
29
|
+
tags = current_tags
|
30
|
+
if tags.any?
|
31
|
+
tags.collect { |tag| "[#{tag}] " }.join
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def __formatter
|
36
|
+
@formatter
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Compatibility with ActiveSupport::TaggedLogging which only supports adding tags as strings.
|
41
|
+
# If a tag looks like "key:value" or "key=value", it will be added as a key value pair.
|
42
|
+
# Otherwise it will be appended to a list named "tagged".
|
43
|
+
def tagged(*tags, &block)
|
44
|
+
tag_hash = {}
|
45
|
+
tags.flatten.each do |tag|
|
46
|
+
tagged_values = Array(tag_hash["tagged"] || self.tags["tagged"])
|
47
|
+
tag_hash["tagged"] = tagged_values + [tag]
|
48
|
+
end
|
49
|
+
tag(tag_hash, &block)
|
50
|
+
end
|
51
|
+
|
52
|
+
def push_tags(*tags)
|
53
|
+
tagged(*tags)
|
54
|
+
end
|
55
|
+
|
56
|
+
def pop_tags(size = 1)
|
57
|
+
tagged_values = Array(@tags["tagged"])
|
58
|
+
tagged_values = (tagged_values.size > size ? tagged_values[0, tagged_values.size - size] : nil)
|
59
|
+
tag("tagged" => tagged_values)
|
60
|
+
end
|
61
|
+
|
62
|
+
def clear_tags!
|
63
|
+
tag("tagged" => nil)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lumberjack
|
4
|
+
# Monkey patch for ActiveSupport::TaggedLogger so it doesn't blow up when
|
5
|
+
# a Lumberjack logger is trying to be wrapped. This module will be automatically
|
6
|
+
# included in ActiveSupport::TaggedLogger if activesupport is already loaded.
|
7
|
+
module TaggedLogging
|
8
|
+
class << self
|
9
|
+
def included(base)
|
10
|
+
base.singleton_class.send(:prepend, ClassMethods)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def new(logger)
|
16
|
+
if logger.is_a?(Lumberjack::Logger)
|
17
|
+
logger = logger.tagged_logger! unless logger.respond_to?(:tagged)
|
18
|
+
logger
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
if defined?(ActiveSupport::TaggedLogging)
|
28
|
+
ActiveSupport::TaggedLogging.include(Lumberjack::TaggedLogging)
|
29
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lumberjack
|
4
|
+
class Tags
|
5
|
+
class << self
|
6
|
+
# Transform hash keys to strings. This method exists for optimization and backward compatibility.
|
7
|
+
# If a hash already has string keys, it will be returned as is.
|
8
|
+
def stringify_keys(hash)
|
9
|
+
return nil if hash.nil?
|
10
|
+
if hash.keys.all? { |key| key.is_a?(String) }
|
11
|
+
hash
|
12
|
+
elsif hash.respond_to?(:transform_keys)
|
13
|
+
hash.transform_keys(&:to_s)
|
14
|
+
else
|
15
|
+
copy = {}
|
16
|
+
hash.each do |key, value|
|
17
|
+
copy[key.to_s] = value
|
18
|
+
end
|
19
|
+
copy
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Ensure keys are strings and expand any values in a hash that are Proc's by calling them and replacing
|
24
|
+
# the value with the result. This allows setting global tags with runtime values.
|
25
|
+
def expand_runtime_values(hash)
|
26
|
+
return nil if hash.nil?
|
27
|
+
if hash.all? { |key, value| key.is_a?(String) && !value.is_a?(Proc) }
|
28
|
+
return hash
|
29
|
+
end
|
30
|
+
|
31
|
+
copy = {}
|
32
|
+
hash.each do |key, value|
|
33
|
+
if value.is_a?(Proc) && (value.arity == 0 || value.arity == -1)
|
34
|
+
value = value.call
|
35
|
+
end
|
36
|
+
copy[key.to_s] = value
|
37
|
+
end
|
38
|
+
copy
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/lumberjack/template.rb
CHANGED
@@ -4,70 +4,118 @@ module Lumberjack
|
|
4
4
|
# A template converts entries to strings. Templates can contain the following place holders to
|
5
5
|
# reference log entry values:
|
6
6
|
#
|
7
|
-
# *
|
8
|
-
# *
|
9
|
-
# *
|
10
|
-
# *
|
11
|
-
# *
|
7
|
+
# * :time
|
8
|
+
# * :severity
|
9
|
+
# * :progname
|
10
|
+
# * :tags
|
11
|
+
# * :message
|
12
|
+
#
|
13
|
+
# Any other words prefixed with a colon will be substituted with the value of the tag with that name.
|
14
|
+
# If your tag name contains characters other than alpha numerics and the underscore, you must surround it
|
15
|
+
# with curly brackets: `:{http.request-id}`.
|
12
16
|
class Template
|
13
|
-
TEMPLATE_ARGUMENT_ORDER = %w
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
17
|
+
TEMPLATE_ARGUMENT_ORDER = %w[:time :severity :progname :pid :message :tags].freeze
|
18
|
+
MILLISECOND_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%3N"
|
19
|
+
MICROSECOND_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%6N"
|
20
|
+
PLACEHOLDER_PATTERN = /:(([a-z0-9_]+)|({[^}]+}))/i.freeze
|
21
|
+
|
18
22
|
# Create a new template from the markup. The +first_line+ argument is used to format only the first
|
19
23
|
# line of a message. Additional lines will be added to the message unformatted. If you wish to format
|
20
|
-
# the additional lines, use the
|
24
|
+
# the additional lines, use the :additional_lines options to specify a template. Note that you'll need
|
21
25
|
# to provide the line separator character in this template if you want to keep the message on multiple lines.
|
22
26
|
#
|
23
27
|
# The time will be formatted as YYYY-MM-DDTHH:MM:SSS.SSS by default. If you wish to change the format, you
|
24
|
-
# can specify the
|
28
|
+
# can specify the :time_format option which can be either a time format template as documented in
|
25
29
|
# +Time#strftime+ or the values +:milliseconds+ or +:microseconds+ to use the standard format with the
|
26
30
|
# specified precision.
|
27
31
|
#
|
28
32
|
# Messages will have white space stripped from both ends.
|
29
33
|
def initialize(first_line, options = {})
|
30
|
-
@first_line_template = compile(first_line)
|
34
|
+
@first_line_template, @first_line_tags = compile(first_line)
|
31
35
|
additional_lines = options[:additional_lines] || "#{Lumberjack::LINE_SEPARATOR}:message"
|
32
|
-
@additional_line_template = compile(additional_lines)
|
36
|
+
@additional_line_template, @additional_line_tags = compile(additional_lines)
|
33
37
|
# Formatting the time is relatively expensive, so only do it if it will be used
|
34
38
|
@template_include_time = first_line.include?(":time") || additional_lines.include?(":time")
|
35
|
-
|
39
|
+
self.datetime_format = (options[:time_format] || :milliseconds)
|
40
|
+
end
|
41
|
+
|
42
|
+
def datetime_format=(format)
|
43
|
+
if format == :milliseconds
|
44
|
+
format = MILLISECOND_TIME_FORMAT
|
45
|
+
elsif format == :microseconds
|
46
|
+
format = MICROSECOND_TIME_FORMAT
|
47
|
+
end
|
48
|
+
@time_formatter = Formatter::DateTimeFormatter.new(format)
|
36
49
|
end
|
37
|
-
|
50
|
+
|
51
|
+
def datetime_format
|
52
|
+
@time_formatter.format
|
53
|
+
end
|
54
|
+
|
38
55
|
# Convert an entry into a string using the template.
|
39
56
|
def call(entry)
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
57
|
+
return entry unless entry.is_a?(LogEntry)
|
58
|
+
|
59
|
+
first_line = entry.message.to_s
|
60
|
+
additional_lines = nil
|
61
|
+
if first_line.include?(Lumberjack::LINE_SEPARATOR)
|
62
|
+
additional_lines = first_line.split(Lumberjack::LINE_SEPARATOR)
|
63
|
+
first_line = additional_lines.shift
|
64
|
+
end
|
65
|
+
|
66
|
+
formatted_time = @time_formatter.call(entry.time) if @template_include_time
|
67
|
+
format_args = [formatted_time, entry.severity_label, entry.progname, entry.pid, first_line]
|
68
|
+
tag_arguments = tag_args(entry.tags, @first_line_tags)
|
69
|
+
message = (@first_line_template % (format_args + tag_arguments))
|
70
|
+
message.rstrip! if message.end_with?(" ")
|
71
|
+
|
72
|
+
if additional_lines && !additional_lines.empty?
|
73
|
+
tag_arguments = tag_args(entry.tags, @additional_line_tags) unless @additional_line_tags == @first_line_tags
|
74
|
+
additional_lines.each do |line|
|
75
|
+
format_args[format_args.size - 1] = line
|
76
|
+
line_message = (@additional_line_template % (format_args + tag_arguments)).rstrip
|
77
|
+
line_message.rstrip! if line_message.end_with?(" ")
|
78
|
+
message << line_message
|
79
|
+
end
|
45
80
|
end
|
46
81
|
message
|
47
82
|
end
|
48
|
-
|
83
|
+
|
49
84
|
private
|
50
85
|
|
51
|
-
def
|
52
|
-
if
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
86
|
+
def tag_args(tags, tag_vars)
|
87
|
+
return [nil] * (tag_vars.size + 1) if tags.nil? || tags.size == 0
|
88
|
+
|
89
|
+
tags_string = ""
|
90
|
+
tags.each do |name, value|
|
91
|
+
unless value.nil? || tag_vars.include?(name)
|
92
|
+
value = value.to_s
|
93
|
+
value = value.gsub(Lumberjack::LINE_SEPARATOR, " ") if value.include?(Lumberjack::LINE_SEPARATOR)
|
94
|
+
tags_string << "[#{name}:#{value}] "
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
args = [tags_string.chop]
|
99
|
+
tag_vars.each do |name|
|
100
|
+
args << tags[name]
|
58
101
|
end
|
102
|
+
args
|
59
103
|
end
|
60
|
-
|
104
|
+
|
61
105
|
# Compile the template string into a value that can be used with sprintf.
|
62
106
|
def compile(template) #:nodoc:
|
63
|
-
|
64
|
-
|
107
|
+
tag_vars = []
|
108
|
+
template = template.gsub(PLACEHOLDER_PATTERN) do |match|
|
109
|
+
var_name = match.sub("{", "").sub("}", "")
|
110
|
+
position = TEMPLATE_ARGUMENT_ORDER.index(var_name)
|
65
111
|
if position
|
66
112
|
"%#{position + 1}$s"
|
67
113
|
else
|
68
|
-
|
114
|
+
tag_vars << var_name[1, var_name.length]
|
115
|
+
"%#{TEMPLATE_ARGUMENT_ORDER.size + tag_vars.size}$s"
|
69
116
|
end
|
70
117
|
end
|
118
|
+
[template, tag_vars]
|
71
119
|
end
|
72
120
|
end
|
73
121
|
end
|