diff --git a/app/assets/javascripts/vue/in_tail_format.js b/app/assets/javascripts/vue/in_tail_format.js index 2e79ffe..a5944e6 100644 --- a/app/assets/javascripts/vue/in_tail_format.js +++ b/app/assets/javascripts/vue/in_tail_format.js @@ -1,5 +1,6 @@ (function(){ "use strict"; + var maxFormatCount = 20; $(function(){ if($('#in_tail_format').length === 0) return; @@ -30,7 +31,20 @@ highlightedLines: null, }, + computed: { + useTextArea: function() { + return this.format === "multiline"; + } + }, + compiled: function(){ + this.$watch('params.setting.formats', function(formats){ + _.range(1, maxFormatCount + 1).forEach(function(i) {params.setting["format" + String(i)] = "";}); + + _.compact(formats.split("\n")).forEach(function(formatLine, index) { + params.setting["format" + String(index + 1)] = formatLine; + }); + }), this.$watch('params.setting.regexp', function(){ this.preview(); }); @@ -46,6 +60,10 @@ if(!params.setting) { params.setting = {}; } + + var formats = _.chain(_.range(1, maxFormatCount + 1)).map(function(i) {return params.setting["format" + String(i)];}).compact().value(); + params.setting.formats = formats.join("\n"); + _.each(this.formatOptions, function(options){ _.each(options, function(key){ if(!params.setting.hasOwnProperty(key)){ @@ -58,6 +76,12 @@ }, methods: { + onKeyup: function(ev){ + var el = ev.target; + if(el.name.match(/\[format/)){ + this.preview(); + } + }, updateHighlightedLines: function() { if(!this.regexpMatches) { this.highlightedLines = null; @@ -132,6 +156,7 @@ regexp: self.params.setting.regexp, time_format: self.params.setting.time_format, format: _.isEmpty(self.format) ? "regexp" : self.format, + params: self.params.setting, file: self.targetFile } }).done(resolve).fail(reject); diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 998b784..c162390 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -19,17 +19,9 @@ class ApiController < ApplicationController end def regexp_preview - preview = RegexpPreview.new(params[:file], params[:format], regexp: params[:regexp], time_format: params[:time_format]) - matches = preview.matches - render json: { - params: { - setting: { - regexp: preview.regexp.try(:source), - time_format: preview.time_format, - } - }, - matches: matches.compact, - } + preview = RegexpPreview.processor(params[:format]).new(params[:file], params[:format], params) + + render json: preview.matches_json end def grok_to_regexp diff --git a/app/models/fluentd/setting/in_tail.rb b/app/models/fluentd/setting/in_tail.rb index e5a009f..1d160bb 100644 --- a/app/models/fluentd/setting/in_tail.rb +++ b/app/models/fluentd/setting/in_tail.rb @@ -1,6 +1,8 @@ class Fluentd module Setting class InTail + MULTI_LINE_MAX_FORMAT_COUNT = 20 + include ActiveModel::Model attr_accessor :path, :tag, :format, :regexp, :time_format, :rotate_wait, :pos_file, :read_from_head, :refresh_interval @@ -18,12 +20,13 @@ class Fluentd :ltsv => [:delimiter, :time_key], :json => [:time_key], :regexp => [:time_format, :regexp], + :multiline => [:format_firstline] + (1..MULTI_LINE_MAX_FORMAT_COUNT).map{|n| "format#{n}".to_sym } # TODO: Grok could generate Regexp including \d, \s, etc. fluentd config parser raise error with them for escape sequence check. # TBD How to handle Grok/Regexp later, just comment out for hide # :grok => [:grok_str], } end - attr_accessor *known_formats.values.flatten.compact + attr_accessor *known_formats.values.flatten.compact.uniq def known_formats self.class.known_formats @@ -59,9 +62,20 @@ class Fluentd indent = " " * 2 format_specific_conf = "" - extra_format_options.each do |key| - format_specific_conf << "#{indent}#{key} #{send(key)}\n" + + if format.to_sym == :multiline + known_formats[:multiline].each do |key| + value = send(key) + if value.present? + format_specific_conf << "#{indent}#{key} /#{value}/\n" + end + end + else + extra_format_options.each do |key| + format_specific_conf << "#{indent}#{key} #{send(key)}\n" + end end + format_specific_conf end diff --git a/app/views/fluentd/settings/in_tail/after_file_choose.html.haml b/app/views/fluentd/settings/in_tail/after_file_choose.html.haml index 79da579..8c494c9 100644 --- a/app/views/fluentd/settings/in_tail/after_file_choose.html.haml +++ b/app/views/fluentd/settings/in_tail/after_file_choose.html.haml @@ -12,7 +12,7 @@ = f.text_field :path, class: "form-control", disabled: true = render partial: "shared/vue/in_tail_format", locals: { file: f.object.path, formats: @setting.known_formats, initialSelected: f.object.format || @setting.guess_format } - %pre= file_tail(@setting.path).join("\n") + %pre= file_tail(@setting.path, Settings.in_tail_preview_line_count).join("\n") %p = f.submit t('terms.next'), class: "btn btn-lg btn-primary pull-right" diff --git a/app/views/shared/vue/_in_tail_format.html.erb b/app/views/shared/vue/_in_tail_format.html.erb index 8537739..eb642c6 100644 --- a/app/views/shared/vue/_in_tail_format.html.erb +++ b/app/views/shared/vue/_in_tail_format.html.erb @@ -6,8 +6,21 @@
<%= t("fluentd.settings.in_tail.notice_for_multiline_limit") %>
+ + +
diff --git a/config/locales/translation_ja.yml b/config/locales/translation_ja.yml
index c8da92b..9a41a0b 100644
--- a/config/locales/translation_ja.yml
+++ b/config/locales/translation_ja.yml
@@ -216,6 +216,7 @@ ja:
in_tailプラグインの解説ページや
Fluentularもご参照ください。
in_tail:
+ notice_for_multiline_limit: "改行区切りで正規表現を入力してください。空行はカウントされません。21行目以降の入力は無視されます。"
notice_for_permission: "※%{user}ユーザーが読み込み可能なようにパーミッションやグループの設定をご確認ください。"
restart_from_first: 最初からやり直す
grok_manual: |
diff --git a/lib/regexp_preview.rb b/lib/regexp_preview.rb
index 74af30b..bb0c81f 100644
--- a/lib/regexp_preview.rb
+++ b/lib/regexp_preview.rb
@@ -2,47 +2,16 @@
require "fluent/registry"
require "fluent/configurable"
require "fluent/parser"
+require "regexp_preview/single_line"
+require "regexp_preview/multi_line"
-class RegexpPreview
- attr_reader :file, :format, :time_format, :regexp
-
- def initialize(file, format, options = {})
- @file = file
- @format = format
+module RegexpPreview
+ def self.processor(format)
case format
- when "regexp"
- @regexp = Regexp.new(options[:regexp])
- @time_format = options[:time_format]
- when "ltsv", "json", "csv", "tsv"
+ when "multiline"
+ RegexpPreview::MultiLine
else
- definition = Fluent::TextParser::TEMPLATE_REGISTRY.lookup(format).call
- raise "Unknown format '#{format}'" unless definition
- definition.configure({}) # NOTE: SyslogParser define @regexp in configure method so call it to grab Regexp object
- @regexp = definition.patterns["format"]
- @time_format = definition.patterns["time_format"]
+ RegexpPreview::SingleLine
end
end
-
- def matches
- return [] unless @regexp # such as ltsv, json, etc
- reader = FileReverseReader.new(File.open(file))
- matches = reader.tail.map do |line|
- result = {
- :whole => line,
- :matches => [],
- }
- m = line.match(regexp)
- next result unless m
-
- m.names.each_with_index do |name, index|
- result[:matches] << {
- key: name,
- matched: m[name],
- pos: m.offset(index + 1),
- }
- end
- result
- end
- matches
- end
end
diff --git a/lib/regexp_preview/multi_line.rb b/lib/regexp_preview/multi_line.rb
new file mode 100644
index 0000000..7c291bb
--- /dev/null
+++ b/lib/regexp_preview/multi_line.rb
@@ -0,0 +1,70 @@
+module RegexpPreview
+ class MultiLine
+ attr_reader :file, :format, :params
+
+ def initialize(file, format, params = {})
+ @file = file
+ @format = format
+ @params = params[:params]
+ end
+
+ def matches_json
+ {
+ params: {
+ setting: { # for vue.js
+ regexp: nil,
+ time_format: nil,
+ }
+ },
+ matches: matches.compact,
+ }
+ end
+
+ private
+
+ def matches
+ return [] if patterns.empty?
+ reader = FileReverseReader.new(File.open(file))
+ result = []
+ target_lines = reader.tail(Settings.in_tail_preview_line_count).map{|line| line << "\n" }
+ target_lines.each_with_index do |line, line_no|
+ if line.match(params[:format_firstline])
+ lines = target_lines[line_no, patterns.length]
+ next if lines.length < patterns.length
+ ret = detect_chunk(lines)
+ next unless ret
+ result << ret
+ end
+ end
+ result
+ end
+
+ def detect_chunk(lines)
+ whole = ""
+ matches = []
+ lines.each_with_index do |line, i|
+ match = line.match(patterns[i])
+ return nil unless match
+ match.names.each_with_index do |name, index|
+ matches << {
+ key: name,
+ matched: match[name],
+ pos: match.offset(index + 1).map{|pos| pos + whole.length},
+ }
+ end
+ whole << line
+ end
+ {
+ whole: whole,
+ matches: matches,
+ }
+ end
+
+ def patterns
+ @patterns ||= (1..20).map do |n|
+ params["format#{n}"].presence
+ end.compact.map {|pattern| Regexp.new(pattern)}
+ end
+ end
+end
+
diff --git a/lib/regexp_preview/single_line.rb b/lib/regexp_preview/single_line.rb
new file mode 100644
index 0000000..1981e73
--- /dev/null
+++ b/lib/regexp_preview/single_line.rb
@@ -0,0 +1,65 @@
+module RegexpPreview
+ class SingleLine
+ attr_reader :file, :format, :params, :regexp, :time_format
+
+ def initialize(file, format, params = {})
+ @file = file
+ @format = format
+ @time_format = params[:time_format]
+ @params = params
+
+ case format
+ when "regexp"
+ @regexp = Regexp.new(params[:regexp])
+ @time_format = nil
+ when "ltsv", "json", "csv", "tsv"
+ @regexp = nil
+ @time_format = nil
+ else # apache, nginx, etc
+ definition = Fluent::TextParser::TEMPLATE_REGISTRY.lookup(format).call
+ raise "Unknown format '#{format}'" unless definition
+ definition.configure({}) # NOTE: SyslogParser define @regexp in configure method so call it to grab Regexp object
+ @regexp = definition.patterns["format"]
+ @time_format = definition.patterns["time_format"]
+ end
+ end
+
+ def matches_json
+ {
+ params: {
+ setting: {
+ # NOTE: regexp and time_format are used when format == 'apache' || 'nginx' || etc.
+ regexp: regexp.try(:source),
+ time_format: time_format,
+ }
+ },
+ matches: matches.compact,
+ }
+ end
+
+ private
+
+ def matches
+ return [] unless @regexp # such as ltsv, json, etc
+ reader = FileReverseReader.new(File.open(file))
+ matches = reader.tail(Settings.in_tail_preview_line_count).map do |line|
+ result = {
+ :whole => line,
+ :matches => [],
+ }
+ match = line.match(regexp)
+ next result unless match
+
+ match.names.each_with_index do |name, index|
+ result[:matches] << {
+ key: name,
+ matched: match[name],
+ pos: match.offset(index + 1),
+ }
+ end
+ result
+ end
+ matches
+ end
+ end
+end
diff --git a/spec/lib/regexp_preview/multi_line_spec.rb b/spec/lib/regexp_preview/multi_line_spec.rb
new file mode 100644
index 0000000..100c6ff
--- /dev/null
+++ b/spec/lib/regexp_preview/multi_line_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe RegexpPreview::MultiLine do
+ describe "#matches_json" do
+ subject { RegexpPreview::MultiLine.new(File.expand_path("./spec/support/fixtures/error0.log", Rails.root), "multiline", params).matches_json }
+
+ let :params do
+ params = {
+ format_firstline: ".+",
+ time_format: "time_format",
+ }
+ params["format1"] = "(?