ngmoco-request-log-analyzer 1.4.2

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 (112) hide show
  1. data/.gitignore +10 -0
  2. data/DESIGN.rdoc +41 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +39 -0
  5. data/Rakefile +8 -0
  6. data/bin/request-log-analyzer +114 -0
  7. data/lib/cli/command_line_arguments.rb +301 -0
  8. data/lib/cli/database_console.rb +26 -0
  9. data/lib/cli/database_console_init.rb +43 -0
  10. data/lib/cli/progressbar.rb +213 -0
  11. data/lib/cli/tools.rb +46 -0
  12. data/lib/request_log_analyzer.rb +44 -0
  13. data/lib/request_log_analyzer/aggregator.rb +49 -0
  14. data/lib/request_log_analyzer/aggregator/database_inserter.rb +83 -0
  15. data/lib/request_log_analyzer/aggregator/echo.rb +29 -0
  16. data/lib/request_log_analyzer/aggregator/summarizer.rb +175 -0
  17. data/lib/request_log_analyzer/controller.rb +332 -0
  18. data/lib/request_log_analyzer/database.rb +102 -0
  19. data/lib/request_log_analyzer/database/base.rb +115 -0
  20. data/lib/request_log_analyzer/database/connection.rb +38 -0
  21. data/lib/request_log_analyzer/database/request.rb +22 -0
  22. data/lib/request_log_analyzer/database/source.rb +13 -0
  23. data/lib/request_log_analyzer/database/warning.rb +14 -0
  24. data/lib/request_log_analyzer/file_format.rb +160 -0
  25. data/lib/request_log_analyzer/file_format/amazon_s3.rb +71 -0
  26. data/lib/request_log_analyzer/file_format/apache.rb +141 -0
  27. data/lib/request_log_analyzer/file_format/merb.rb +67 -0
  28. data/lib/request_log_analyzer/file_format/rack.rb +11 -0
  29. data/lib/request_log_analyzer/file_format/rails.rb +176 -0
  30. data/lib/request_log_analyzer/file_format/rails_development.rb +12 -0
  31. data/lib/request_log_analyzer/filter.rb +30 -0
  32. data/lib/request_log_analyzer/filter/anonymize.rb +39 -0
  33. data/lib/request_log_analyzer/filter/field.rb +42 -0
  34. data/lib/request_log_analyzer/filter/timespan.rb +45 -0
  35. data/lib/request_log_analyzer/line_definition.rb +111 -0
  36. data/lib/request_log_analyzer/log_processor.rb +99 -0
  37. data/lib/request_log_analyzer/mailer.rb +62 -0
  38. data/lib/request_log_analyzer/output.rb +113 -0
  39. data/lib/request_log_analyzer/output/fixed_width.rb +220 -0
  40. data/lib/request_log_analyzer/output/html.rb +184 -0
  41. data/lib/request_log_analyzer/request.rb +175 -0
  42. data/lib/request_log_analyzer/source.rb +72 -0
  43. data/lib/request_log_analyzer/source/database_loader.rb +87 -0
  44. data/lib/request_log_analyzer/source/log_parser.rb +274 -0
  45. data/lib/request_log_analyzer/tracker.rb +206 -0
  46. data/lib/request_log_analyzer/tracker/duration.rb +104 -0
  47. data/lib/request_log_analyzer/tracker/frequency.rb +95 -0
  48. data/lib/request_log_analyzer/tracker/hourly_spread.rb +107 -0
  49. data/lib/request_log_analyzer/tracker/timespan.rb +81 -0
  50. data/lib/request_log_analyzer/tracker/traffic.rb +106 -0
  51. data/request-log-analyzer.gemspec +40 -0
  52. data/spec/database.yml +23 -0
  53. data/spec/fixtures/apache_combined.log +5 -0
  54. data/spec/fixtures/apache_common.log +10 -0
  55. data/spec/fixtures/decompression.log +12 -0
  56. data/spec/fixtures/decompression.log.bz2 +0 -0
  57. data/spec/fixtures/decompression.log.gz +0 -0
  58. data/spec/fixtures/decompression.log.zip +0 -0
  59. data/spec/fixtures/decompression.tar.gz +0 -0
  60. data/spec/fixtures/decompression.tgz +0 -0
  61. data/spec/fixtures/header_and_footer.log +6 -0
  62. data/spec/fixtures/merb.log +84 -0
  63. data/spec/fixtures/merb_prefixed.log +9 -0
  64. data/spec/fixtures/multiple_files_1.log +5 -0
  65. data/spec/fixtures/multiple_files_2.log +2 -0
  66. data/spec/fixtures/rails.db +0 -0
  67. data/spec/fixtures/rails_1x.log +59 -0
  68. data/spec/fixtures/rails_22.log +12 -0
  69. data/spec/fixtures/rails_22_cached.log +10 -0
  70. data/spec/fixtures/rails_unordered.log +24 -0
  71. data/spec/fixtures/syslog_1x.log +5 -0
  72. data/spec/fixtures/test_file_format.log +13 -0
  73. data/spec/fixtures/test_language_combined.log +14 -0
  74. data/spec/fixtures/test_order.log +16 -0
  75. data/spec/integration/command_line_usage_spec.rb +84 -0
  76. data/spec/integration/munin_plugins_rails_spec.rb +58 -0
  77. data/spec/integration/scout_spec.rb +151 -0
  78. data/spec/lib/helpers.rb +52 -0
  79. data/spec/lib/macros.rb +18 -0
  80. data/spec/lib/matchers.rb +77 -0
  81. data/spec/lib/mocks.rb +76 -0
  82. data/spec/lib/testing_format.rb +46 -0
  83. data/spec/spec_helper.rb +24 -0
  84. data/spec/unit/aggregator/database_inserter_spec.rb +93 -0
  85. data/spec/unit/aggregator/summarizer_spec.rb +26 -0
  86. data/spec/unit/controller/controller_spec.rb +41 -0
  87. data/spec/unit/controller/log_processor_spec.rb +18 -0
  88. data/spec/unit/database/base_class_spec.rb +183 -0
  89. data/spec/unit/database/connection_spec.rb +34 -0
  90. data/spec/unit/database/database_spec.rb +133 -0
  91. data/spec/unit/file_format/amazon_s3_format_spec.rb +49 -0
  92. data/spec/unit/file_format/apache_format_spec.rb +203 -0
  93. data/spec/unit/file_format/file_format_api_spec.rb +69 -0
  94. data/spec/unit/file_format/line_definition_spec.rb +75 -0
  95. data/spec/unit/file_format/merb_format_spec.rb +52 -0
  96. data/spec/unit/file_format/rails_format_spec.rb +164 -0
  97. data/spec/unit/filter/anonymize_filter_spec.rb +21 -0
  98. data/spec/unit/filter/field_filter_spec.rb +66 -0
  99. data/spec/unit/filter/filter_spec.rb +17 -0
  100. data/spec/unit/filter/timespan_filter_spec.rb +58 -0
  101. data/spec/unit/mailer_spec.rb +30 -0
  102. data/spec/unit/request_spec.rb +111 -0
  103. data/spec/unit/source/log_parser_spec.rb +119 -0
  104. data/spec/unit/tracker/duration_tracker_spec.rb +130 -0
  105. data/spec/unit/tracker/frequency_tracker_spec.rb +88 -0
  106. data/spec/unit/tracker/hourly_spread_spec.rb +79 -0
  107. data/spec/unit/tracker/timespan_tracker_spec.rb +73 -0
  108. data/spec/unit/tracker/tracker_api_spec.rb +124 -0
  109. data/spec/unit/tracker/traffic_tracker_spec.rb +107 -0
  110. data/tasks/github-gem.rake +323 -0
  111. data/tasks/request_log_analyzer.rake +26 -0
  112. metadata +220 -0
@@ -0,0 +1,102 @@
1
+ require 'rubygems'
2
+ require 'activerecord'
3
+
4
+ class RequestLogAnalyzer::Database
5
+
6
+ def self.const_missing(const) # :nodoc:
7
+ RequestLogAnalyzer::load_default_class_file(self, const)
8
+ end
9
+
10
+ include RequestLogAnalyzer::Database::Connection
11
+
12
+ attr_accessor :file_format
13
+ attr_reader :line_classes
14
+
15
+ def initialize(connection_identifier = nil)
16
+ @line_classes = []
17
+ RequestLogAnalyzer::Database::Base.database = self
18
+ connect(connection_identifier)
19
+ end
20
+
21
+ # Returns the ORM class for the provided line type
22
+ def get_class(line_type)
23
+ line_type = line_type.name if line_type.respond_to?(:name)
24
+ Object.const_get("#{line_type}_line".camelize)
25
+ end
26
+
27
+ def default_classes
28
+ [RequestLogAnalyzer::Database::Request, RequestLogAnalyzer::Database::Source, RequestLogAnalyzer::Database::Warning]
29
+ end
30
+
31
+ # Loads the ORM classes by inspecting the tables in the current database
32
+ def load_database_schema!
33
+ connection.tables.map do |table|
34
+ case table.to_sym
35
+ when :warnings then RequestLogAnalyzer::Database::Warning
36
+ when :sources then RequestLogAnalyzer::Database::Source
37
+ when :requests then RequestLogAnalyzer::Database::Request
38
+ else load_activerecord_class(table)
39
+ end
40
+ end
41
+ end
42
+
43
+ # Returns an array of all the ActiveRecord-bases ORM classes for this database
44
+ def orm_classes
45
+ default_classes + line_classes
46
+ end
47
+
48
+ # Loads an ActiveRecord-based class that correspond to the given parameter, which can either be
49
+ # a table name or a LineDefinition instance.
50
+ def load_activerecord_class(linedefinition_or_table)
51
+
52
+ case linedefinition_or_table
53
+ when String, Symbol
54
+ klass_name = linedefinition_or_table.to_s.singularize.camelize
55
+ klass = RequestLogAnalyzer::Database::Base.subclass_from_table(linedefinition_or_table)
56
+ when RequestLogAnalyzer::LineDefinition
57
+ klass_name = "#{linedefinition_or_table.name}_line".camelize
58
+ klass = RequestLogAnalyzer::Database::Base.subclass_from_line_definition(linedefinition_or_table)
59
+ end
60
+
61
+ Object.const_set(klass_name, klass)
62
+ klass = Object.const_get(klass_name)
63
+ @line_classes << klass
64
+ return klass
65
+ end
66
+
67
+ def fileformat_classes
68
+ raise "No file_format provided!" unless file_format
69
+ line_classes = file_format.line_definitions.map { |(name, definition)| load_activerecord_class(definition) }
70
+ return default_classes + line_classes
71
+ end
72
+
73
+ # Creates the database schema and related ActiveRecord::Base subclasses that correspond to the
74
+ # file format definition. These ORM classes will later be used to create records in the database.
75
+ def create_database_schema!
76
+ fileformat_classes.each { |klass| klass.create_table! }
77
+ end
78
+
79
+ # Drops the table of all the ORM classes, and unregisters the classes
80
+ def drop_database_schema!
81
+ file_format ? fileformat_classes.map(&:drop_table!) : orm_classes.map(&:drop_table!)
82
+ remove_orm_classes!
83
+ end
84
+
85
+ # Registers the default ORM classes in the default namespace
86
+ def register_default_orm_classes!
87
+ Object.const_set('Request', RequestLogAnalyzer::Database::Request)
88
+ Object.const_set('Source', RequestLogAnalyzer::Database::Source)
89
+ Object.const_set('Warning', RequestLogAnalyzer::Database::Warning)
90
+ end
91
+
92
+ # Unregisters every ORM class constant
93
+ def remove_orm_classes!
94
+ orm_classes.each do |klass|
95
+ if klass.respond_to?(:name) && !klass.name.blank?
96
+ klass_name = klass.name.split('::').last
97
+ Object.send(:remove_const, klass_name) if Object.const_defined?(klass_name)
98
+ end
99
+ end
100
+ end
101
+
102
+ end
@@ -0,0 +1,115 @@
1
+ class RequestLogAnalyzer::Database::Base < ActiveRecord::Base
2
+
3
+ self.abstract_class = true
4
+
5
+ def <=>(other)
6
+ if (source_id.nil? && other.source_id.nil?) || (source_comparison = source_id <=> other.source_id) == 0
7
+ lineno <=> other.lineno
8
+ else
9
+ source_comparison
10
+ end
11
+ end
12
+
13
+ def line_type
14
+ self.class.name.underscore.gsub(/_line$/, '').to_sym
15
+ end
16
+
17
+ class_inheritable_accessor :line_definition
18
+ cattr_accessor :database
19
+
20
+ def self.subclass_from_line_definition(definition)
21
+ klass = Class.new(RequestLogAnalyzer::Database::Base)
22
+ klass.set_table_name("#{definition.name}_lines")
23
+
24
+ klass.line_definition = definition
25
+
26
+ # Set relations with requests and sources table
27
+ klass.belongs_to :request
28
+ klass.belongs_to :source
29
+
30
+ # Serialize complex fields into the database
31
+ definition.captures.select { |c| c.has_key?(:provides) }.each do |capture|
32
+ klass.send(:serialize, capture[:name], Hash)
33
+ end
34
+
35
+ RequestLogAnalyzer::Database::Request.has_many "#{definition.name}_lines".to_sym
36
+ RequestLogAnalyzer::Database::Source.has_many "#{definition.name}_lines".to_sym
37
+
38
+ return klass
39
+ end
40
+
41
+ def self.subclass_from_table(table)
42
+ raise "Table #{table} not found!" unless database.connection.table_exists?(table)
43
+
44
+ klass = Class.new(RequestLogAnalyzer::Database::Base)
45
+ klass.set_table_name(table)
46
+
47
+ if klass.column_names.include?('request_id')
48
+ klass.belongs_to :request
49
+ RequestLogAnalyzer::Database::Request.has_many table.to_sym
50
+ end
51
+
52
+ if klass.column_names.include?('source_id')
53
+ klass.belongs_to :source
54
+ RequestLogAnalyzer::Database::Source.has_many table.to_sym
55
+ end
56
+
57
+ return klass
58
+ end
59
+
60
+ def self.drop_table!
61
+ database.connection.remove_index(self.table_name, [:source_id]) rescue nil
62
+ database.connection.remove_index(self.table_name, [:request_id]) rescue nil
63
+ database.connection.drop_table(self.table_name) if database.connection.table_exists?(self.table_name)
64
+ end
65
+
66
+ def self.create_table!
67
+ raise "No line_definition available to base table schema on!" unless self.line_definition
68
+
69
+ unless table_exists?
70
+ database.connection.create_table(table_name.to_sym) do |t|
71
+
72
+ # Default fields
73
+ t.column :request_id, :integer
74
+ t.column :source_id, :integer
75
+ t.column :lineno, :integer
76
+
77
+ line_definition.captures.each do |capture|
78
+ # Add a field for every capture
79
+ t.column(capture[:name], column_type(capture[:type]))
80
+
81
+ # If the capture provides other field as well, create columns for them, too
82
+ capture[:provides].each { |field, field_type| t.column(field, column_type(field_type)) } if capture[:provides].kind_of?(Hash)
83
+ end
84
+ end
85
+ end
86
+
87
+ # Add indices to table for more speedy querying
88
+ database.connection.add_index(self.table_name.to_sym, [:request_id]) # rescue
89
+ database.connection.add_index(self.table_name.to_sym, [:source_id]) # rescue
90
+ end
91
+
92
+
93
+ # Function to determine the column type for a field
94
+ # TODO: make more robust / include in file-format definition
95
+ def self.column_type(type_indicator)
96
+ case type_indicator
97
+ when :eval; :text
98
+ when :hash; :text
99
+ when :text; :text
100
+ when :string; :string
101
+ when :sec; :double
102
+ when :msec; :double
103
+ when :duration; :double
104
+ when :float; :double
105
+ when :double; :double
106
+ when :integer; :integer
107
+ when :int; :int
108
+ when :timestamp; :datetime
109
+ when :datetime; :datetime
110
+ when :date; :date
111
+ else :string
112
+ end
113
+ end
114
+
115
+ end
@@ -0,0 +1,38 @@
1
+ module RequestLogAnalyzer::Database::Connection
2
+
3
+ def self.from_string(string)
4
+ hash = {}
5
+ if string =~ /^(?:\w+=(?:[^;])*;)*\w+=(?:[^;])*$/
6
+ string.scan(/(\w+)=([^;]*);?/) { |variable, value| hash[variable.to_sym] = value }
7
+ elsif string =~ /^(\w+)\:\/\/(?:(?:([^:]+)(?:\:([^:]+))?\@)?([\w\.-]+)\/)?([\w\:\-\.\/]+)$/
8
+ hash[:adapter], hash[:username], hash[:password], hash[:host], hash[:database] = $1, $2, $3, $4, $5
9
+ hash.delete_if { |k, v| v.nil? }
10
+ end
11
+ return hash.empty? ? nil : hash
12
+ end
13
+
14
+ def connect(connection_identifier)
15
+ if connection_identifier.kind_of?(Hash)
16
+ RequestLogAnalyzer::Database::Base.establish_connection(connection_identifier)
17
+ elsif connection_identifier == ':memory:'
18
+ RequestLogAnalyzer::Database::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
19
+ elsif connection_hash = RequestLogAnalyzer::Database::Connection.from_string(connection_identifier)
20
+ RequestLogAnalyzer::Database::Base.establish_connection(connection_hash)
21
+ elsif connection_identifier.kind_of?(String) # Normal SQLite 3 database file
22
+ RequestLogAnalyzer::Database::Base.establish_connection(:adapter => 'sqlite3', :database => connection_identifier)
23
+ elsif connection_identifier.nil?
24
+ nil
25
+ else
26
+ raise "Cannot connect with this connection_identifier: #{connection_identifier.inspect}"
27
+ end
28
+ end
29
+
30
+ def disconnect
31
+ RequestLogAnalyzer::Database::Base.remove_connection
32
+ end
33
+
34
+ def connection
35
+ RequestLogAnalyzer::Database::Base.connection
36
+ end
37
+
38
+ end
@@ -0,0 +1,22 @@
1
+ class RequestLogAnalyzer::Database::Request < RequestLogAnalyzer::Database::Base
2
+
3
+ # Returns an array of all the Line objects of this request in the correct order.
4
+ def lines
5
+ @lines ||= begin
6
+ lines = []
7
+ self.class.reflections.each { |r, d| lines += self.send(r).all }
8
+ lines.sort
9
+ end
10
+ end
11
+
12
+ # Creates the table to store requests in.
13
+ def self.create_table!
14
+ unless database.connection.table_exists?(:requests)
15
+ database.connection.create_table(:requests) do |t|
16
+ t.column :first_lineno, :integer
17
+ t.column :last_lineno, :integer
18
+ end
19
+ end
20
+ end
21
+
22
+ end
@@ -0,0 +1,13 @@
1
+ class RequestLogAnalyzer::Database::Source < RequestLogAnalyzer::Database::Base
2
+
3
+ def self.create_table!
4
+ unless database.connection.table_exists?(:sources)
5
+ database.connection.create_table(:sources) do |t|
6
+ t.column :filename, :string
7
+ t.column :mtime, :datetime
8
+ t.column :filesize, :integer
9
+ end
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,14 @@
1
+ class RequestLogAnalyzer::Database::Warning < RequestLogAnalyzer::Database::Base
2
+
3
+ def self.create_table!
4
+ unless database.connection.table_exists?(:warnings)
5
+ database.connection.create_table(:warnings) do |t|
6
+ t.column :warning_type, :string, :limit => 30, :null => false
7
+ t.column :message, :string
8
+ t.column :source_id, :integer
9
+ t.column :lineno, :integer
10
+ end
11
+ end
12
+ end
13
+
14
+ end
@@ -0,0 +1,160 @@
1
+ module RequestLogAnalyzer::FileFormat
2
+
3
+ def self.const_missing(const)
4
+ RequestLogAnalyzer::load_default_class_file(self, const)
5
+ end
6
+
7
+ # Loads a FileFormat::Base subclass instance.
8
+ # You can provide:
9
+ # * A FileFormat instance (which will return itself)
10
+ # * A FileFormat class (of which an imstance will be returned)
11
+ # * A filename (from which the FileFormat class is loaded)
12
+ # * A symbol of a built-in file format (e.g. :rails)
13
+ def self.load(file_format, *args)
14
+ klass = nil
15
+ if file_format.kind_of?(RequestLogAnalyzer::FileFormat::Base)
16
+ # this already is a file format! return itself
17
+ return @current_file_format = file_format
18
+
19
+ elsif file_format.kind_of?(Class) && file_format.ancestors.include?(RequestLogAnalyzer::FileFormat::Base)
20
+ # a usable class is provided. Use this format class.
21
+ klass = file_format
22
+
23
+ elsif file_format.kind_of?(String) && File.exist?(file_format)
24
+ # load a format from a ruby file
25
+ require file_format
26
+ const = RequestLogAnalyzer::to_camelcase(File.basename(file_format, '.rb'))
27
+ if RequestLogAnalyzer::FileFormat.const_defined?(const)
28
+ klass = RequestLogAnalyzer::FileFormat.const_get(const)
29
+ elsif Object.const_defined?(const)
30
+ klass = Object.const_get(const)
31
+ else
32
+ raise "Cannot load class #{const} from #{file_format}!"
33
+ end
34
+
35
+ else
36
+ # load a provided file format
37
+ klass = RequestLogAnalyzer::FileFormat.const_get(RequestLogAnalyzer::to_camelcase(file_format))
38
+ end
39
+
40
+ # check the returned klass to see if it can be used
41
+ raise "Could not load a file format from #{file_format.inspect}" if klass.nil?
42
+ raise "Invalid FileFormat class" unless klass.kind_of?(Class) && klass.ancestors.include?(RequestLogAnalyzer::FileFormat::Base)
43
+
44
+ @current_file_format = klass.create(*args) # return an instance of the class
45
+ end
46
+
47
+ # Base class for all log file format definitions. This class provides functions for subclasses to
48
+ # define their LineDefinitions and to define a summary report.
49
+ #
50
+ # A subclass of this class is instantiated when request-log-analyzer is started and this instance
51
+ # is shared with all components of the application so they can act on the specifics of the format
52
+ class Base
53
+
54
+ attr_reader :line_definitions, :report_trackers
55
+
56
+ ####################################################################################
57
+ # CLASS METHODS for format definition
58
+ ####################################################################################
59
+
60
+ # Registers the line definer instance for a subclass.
61
+ def self.inherited(subclass)
62
+ if subclass.superclass == RequestLogAnalyzer::FileFormat::Base
63
+
64
+ # Create aline and report definer for this class
65
+ subclass.class_eval do
66
+ instance_variable_set(:@line_definer, RequestLogAnalyzer::LineDefinition::Definer.new)
67
+ instance_variable_set(:@report_definer, RequestLogAnalyzer::Aggregator::Summarizer::Definer.new)
68
+ class << self; attr_accessor :line_definer, :report_definer; end
69
+ end
70
+
71
+ # Create a custom Request class for this file format
72
+ subclass.const_set('Request', Class.new(RequestLogAnalyzer::Request)) unless subclass.const_defined?('Request')
73
+ else
74
+
75
+ # Copy the line and report definer from the parent class.
76
+ subclass.class_eval do
77
+ instance_variable_set(:@line_definer, superclass.line_definer.clone)
78
+ instance_variable_set(:@report_definer, superclass.report_definer.clone)
79
+ class << self; attr_accessor :line_definer, :report_definer; end
80
+ end
81
+
82
+ # Create a custom Request class based on the superclass's Request class
83
+ subclass.const_set('Request', Class.new(subclass.superclass::Request)) unless subclass.const_defined?('Request')
84
+ end
85
+ end
86
+
87
+ # Specifies a single line defintions.
88
+ def self.line_definition(name, &block)
89
+ @line_definer.send(name, &block)
90
+ end
91
+
92
+ # Specifies multiple line definitions at once using a block
93
+ def self.format_definition(&block)
94
+ if block_given?
95
+ yield self.line_definer
96
+ else
97
+ return self.line_definer
98
+ end
99
+ end
100
+
101
+ # Specifies the summary report using a block.
102
+ def self.report(mode = :append, &block)
103
+ self.report_definer.reset! if mode == :overwrite
104
+ yield(self.report_definer)
105
+ end
106
+
107
+ ####################################################################################
108
+ # Instantiation
109
+ ####################################################################################
110
+
111
+ def self.create(*args)
112
+ # Ignore arguments
113
+ return self.new(line_definer.line_definitions, report_definer.trackers)
114
+ end
115
+
116
+ def initialize(line_definitions = {}, report_trackers = [])
117
+ @line_definitions, @report_trackers = line_definitions, report_trackers
118
+ end
119
+
120
+ ####################################################################################
121
+ # INSTANCE methods
122
+ ####################################################################################
123
+
124
+ # Returns the Request class of this file format
125
+ def request_class
126
+ self.class::Request
127
+ end
128
+
129
+ # Returns a Request instance with the given parsed lines that should be provided as hashes.
130
+ def request(*hashes)
131
+ request_class.create(self, *hashes)
132
+ end
133
+
134
+ # Checks whether the line definitions form a valid language.
135
+ # A file format should have at least a header and a footer line type
136
+ def valid?
137
+ line_definitions.any? { |(name, ld)| ld.header } && line_definitions.any? { |(name, ld)| ld.footer }
138
+ end
139
+
140
+ # Returns true if this language captures the given symbol in one of its line definitions
141
+ def captures?(name)
142
+ line_definitions.any? { |(name, ld)| ld.captures?(name) }
143
+ end
144
+
145
+ # Function that a file format con implement to monkey patch the environment.
146
+ # * <tt>controller</tt> The environment is provided as a controller instance
147
+ def setup_environment(controller)
148
+ end
149
+
150
+ # Parses a line by trying to parse it using every line definition in this file format
151
+ def parse_line(line, &warning_handler)
152
+ self.line_definitions.each do |lt, definition|
153
+ match = definition.matches(line, &warning_handler)
154
+ return match if match
155
+ end
156
+
157
+ return nil
158
+ end
159
+ end
160
+ end