aliyun-log 0.2.2 → 0.2.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b17c3b8727ec3dc44742d220f22f26014a201334889f888d858a0cbc33b9c39
4
- data.tar.gz: df953904cf39fea58a11d99fab15ca115e3187667d062dfacec0a6f1301b2510
3
+ metadata.gz: 8c29e47df03c69ea213e48e43e272714f950887b5ab8efaa60321e4506b4d4bd
4
+ data.tar.gz: fd5dcd273e40f70f93bd69e1fc9dbc392171d90c9881650503c420b18cf0cd5d
5
5
  SHA512:
6
- metadata.gz: dc5e4072953b2e687f7153230d23f3f6c6d68ac80abb52278819531cb6219eec1c0c9abf593e93d2b05dc697d74f6be75d94c522314088c416bb108631290bd7
7
- data.tar.gz: 96b65f80b1fba4284d4472e524d8e424d58fff86ebac92cb281481af51134f8a75bb624d8339f67735038d3d36252bb4a6aa66bb3bc117c8706622a80eccefc1
6
+ metadata.gz: 6a4a990ac42f07d0ce44fe018604a5af553f5d53a74cdb0b3abfd9a6b15f891f434bc3fada83afdae815c3bbf5477d72be9520f6926609ec356fe9aa8b6098d5
7
+ data.tar.gz: 0addf200ac1e5328fd5fbd4a459cf756a52f0b8ad1bc53f775676340147af385dc8758da2b7b0d63c8285b409820e4ab8778a822d0505172d554e12865ad7567
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'version'
3
4
  require_relative 'log/common'
4
5
  require_relative 'log/client'
5
6
  require_relative 'log/config'
@@ -38,7 +38,7 @@ module Aliyun
38
38
  def self.logger
39
39
  unless @logger
40
40
  @logger = Logger.new(
41
- @log_file ||= Config.log_file, MAX_NUM_LOG, ROTATE_SIZE
41
+ @log_file ||= Config.log_file || IO::NULL, MAX_NUM_LOG, ROTATE_SIZE
42
42
  )
43
43
  @logger.level = Logging.logger_level
44
44
  end
@@ -7,8 +7,8 @@ module Aliyun
7
7
  @endpoint = 'https://cn-beijing.log.aliyuncs.com'
8
8
  @open_timeout = 10
9
9
  @read_timeout = 120
10
- @log_file = 'aliyun_log.log'
11
- @log_level = Logger::DEBUG
10
+ @log_level = Logger::WARN
11
+ @log_file = STDOUT
12
12
  @timestamps = true
13
13
  class << self
14
14
  attr_accessor :endpoint, :access_key_id, :access_key_secret,
@@ -0,0 +1,13 @@
1
+ module Aliyun
2
+ module Log
3
+ class LogTail < Common::AttrStruct
4
+ attrs :name, :log_type, :log_path, :file_pattern, :localstore, :time_format,
5
+ :log_begin_regex, :regex, :key, :topic_format,
6
+ :filterKey, :filter_regex, :file_encoding
7
+ def initialize(opts, protocol)
8
+ super(opts)
9
+ @protocol = protocol
10
+ end
11
+ end
12
+ end
13
+ end
@@ -108,7 +108,7 @@ module Aliyun
108
108
  def build_log_pb(attrs, time = Time.now.to_i)
109
109
  logs = attrs.is_a?(Array) ? attrs : [attrs]
110
110
  logs.map do |log_attr|
111
- contents = log_attr.compact.map { |k, v| { key: k, value: v.to_s } }
111
+ contents = log_attr.map { |k, v| { key: k, value: v.to_s } }
112
112
  Protobuf::Log.new(time: time, contents: contents)
113
113
  end
114
114
  end
@@ -182,7 +182,7 @@ module Aliyun
182
182
  keys: {}
183
183
  }
184
184
  fields.each do |k, v|
185
- v[:token] = INDEX_DEFAULT_TOKEN if %w[text json].include?(v[:type].to_s) && v[:token].blank?
185
+ v[:token] = INDEX_DEFAULT_TOKEN if %w[text json].include?(v[:type].to_s) && v[:token].nil?
186
186
  body[:keys][k] = v
187
187
  end
188
188
  @http.post({ project: project_name, logstore: logstore_name, action: 'index' }, body.to_json)
@@ -196,7 +196,7 @@ module Aliyun
196
196
  keys: {}
197
197
  }
198
198
  fields.each do |k, v|
199
- v[:token] = INDEX_DEFAULT_TOKEN if %w[text json].include?(v[:type].to_s) && v[:token].blank?
199
+ v[:token] = INDEX_DEFAULT_TOKEN if %w[text json].include?(v[:type].to_s) && v[:token].nil?
200
200
  body[:keys][k] = v
201
201
  end
202
202
  @http.put({ project: project_name, logstore: logstore_name, action: 'index' }, body.to_json)
@@ -9,7 +9,7 @@ require 'forwardable'
9
9
  require_relative 'record/exception'
10
10
  require_relative 'record/field'
11
11
  require_relative 'record/persistence'
12
- require_relative 'record/relation'
12
+ require_relative 'record/scoping'
13
13
 
14
14
  module Aliyun
15
15
  module Log
@@ -27,45 +27,30 @@ module Aliyun
27
27
 
28
28
  Log.included_models << self unless Log.included_models.include? self
29
29
 
30
- field :created_at, :text if Config.timestamps
30
+ field :created_at, type: :text, cast_type: :datetime if Config.timestamps
31
31
 
32
32
  define_model_callbacks :save, :create, :initialize
33
33
 
34
- after_initialize :set_created_at
34
+ before_save :set_created_at
35
35
  end
36
36
 
37
37
  include Field
38
38
  include Persistence
39
+ include Scoping
39
40
 
40
41
  include ActiveModel::AttributeMethods
41
42
 
42
43
  module ClassMethods
43
44
  def logstore(options = {})
44
- if options[:timestamps] && !Config.timestamps
45
+ opt = options.dup
46
+ if opt[:timestamps] && !Config.timestamps
45
47
  field :created_at, :text
46
- elsif options[:timestamps] == false && Config.timestamps
48
+ elsif opt[:timestamps] == false && Config.timestamps
47
49
  remove_field :created_at
48
50
  end
49
- self._schema_load = true if options[:auto_sync] == false
50
- self.options = options
51
- end
52
-
53
- Relation.instance_methods(false).each do |method|
54
- define_method(method) do |args|
55
- relation.public_send(method, args)
56
- end
57
- end
58
-
59
- %i[count load result].each do |method|
60
- define_method(method) do
61
- relation.public_send(method)
62
- end
63
- end
64
-
65
- private
66
-
67
- def relation
68
- Relation.new(self)
51
+ self._schema_load = true if opt[:auto_sync] == false
52
+ opt[:field_doc_value] = opt[:field_doc_value] != false
53
+ self.options = opt
69
54
  end
70
55
  end
71
56
 
@@ -101,7 +86,7 @@ module Aliyun
101
86
  end.compact.join(', ')
102
87
  else
103
88
  'not initialized'
104
- end
89
+ end
105
90
 
106
91
  "#<#{self.class} #{inspection}>"
107
92
  end
@@ -4,6 +4,7 @@ module Aliyun
4
4
  class ArgumentError < StandardError; end
5
5
  class UnknownAttributeError < StandardError; end
6
6
  class ProjectNameError < StandardError; end
7
+ class ParseStatementInvalid < StandardError; end
7
8
  end
8
9
  end
9
10
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require "active_support"
3
- require "active_support/time"
4
- require "active_support/core_ext"
5
- require "active_model"
2
+
3
+ require_relative 'type_casting'
6
4
 
7
5
  module Aliyun
8
6
  module Log
@@ -22,13 +20,16 @@ module Aliyun
22
20
 
23
21
  included do
24
22
  class_attribute :attributes, instance_accessor: false, default: {}
23
+ extend Common::Logging
25
24
  end
26
25
 
27
26
  module ClassMethods
28
- def field(name, type = :text, options = {})
27
+ def field(name, options = {})
28
+ type = options[:type] || :text
29
29
  unless PERMITTED_KEY_TYPES.include?(type)
30
30
  raise ArgumentError, "Field #{name} type(#{type}) error, key type only support text/long/double/json"
31
31
  end
32
+
32
33
  named = name.to_s
33
34
  self.attributes = attributes.merge(name => { type: type }.merge(options))
34
35
 
@@ -36,7 +37,7 @@ module Aliyun
36
37
  warn_about_method_overriding("#{named}=", name)
37
38
  warn_about_method_overriding("#{named}?", name)
38
39
 
39
- define_attribute_method(name) # Dirty API
40
+ define_attribute_method(name)
40
41
 
41
42
  generated_methods.module_eval do
42
43
  define_method(named) { read_attribute(named) }
@@ -64,7 +65,6 @@ module Aliyun
64
65
  remove_method field
65
66
  remove_method :"#{field}="
66
67
  remove_method :"#{field}?"
67
- remove_method :"#{field}_before_type_cast"
68
68
  end
69
69
  end
70
70
 
@@ -80,7 +80,8 @@ module Aliyun
80
80
 
81
81
  def warn_about_method_overriding(method_name, field_name)
82
82
  if instance_methods.include?(method_name.to_sym)
83
- Common::Logging.logger.warn("Method #{method_name} generated for the field #{field_name} overrides already existing method")
83
+ logger.warn("Method #{method_name} generated for the field #{field_name} " \
84
+ 'overrides already existing method')
84
85
  end
85
86
  end
86
87
  end
@@ -106,7 +107,7 @@ module Aliyun
106
107
  attr_accessor :attributes
107
108
 
108
109
  def write_attribute(name, value)
109
- attributes[name.to_sym] = value
110
+ attributes[name.to_sym] = TypeCasting.cast_field(value, self.class.attributes[name.to_sym])
110
111
  end
111
112
 
112
113
  alias []= write_attribute
@@ -117,7 +118,7 @@ module Aliyun
117
118
  alias [] read_attribute
118
119
 
119
120
  def set_created_at
120
- self.created_at ||= DateTime.now.in_time_zone(Time.zone).to_s if timestamps_enabled?
121
+ self.created_at ||= DateTime.now.in_time_zone(Time.zone).iso8601 if timestamps_enabled?
121
122
  end
122
123
 
123
124
  def timestamps_enabled?
@@ -8,7 +8,8 @@ module Aliyun
8
8
 
9
9
  module ClassMethods
10
10
  def logstore_name
11
- @logstore_name ||= options[:name] || base_class.name.split('::').last.downcase.pluralize
11
+ @logstore_name ||= options[:name] ||
12
+ base_class.name.split('::').last.underscore.pluralize
12
13
  end
13
14
 
14
15
  def logstore_name=(value)
@@ -34,6 +35,7 @@ module Aliyun
34
35
  end
35
36
 
36
37
  def sync_index
38
+ return if field_indices.blank?
37
39
  has_index? ? update_index : create_index
38
40
  end
39
41
 
@@ -55,9 +57,8 @@ module Aliyun
55
57
  def create(data, opts = {})
56
58
  auto_load_schema
57
59
  if data.is_a?(Array)
58
- logs = []
59
- data.each do |log_attr|
60
- logs << new(log_attr).save_array
60
+ logs = data.map do |log_attr|
61
+ new(log_attr).save_array
61
62
  end
62
63
  res = Log.record_connection.put_log(project_name, logstore_name, logs, opts)
63
64
  res.code == 200
@@ -79,10 +80,10 @@ module Aliyun
79
80
  end
80
81
 
81
82
  def field_indices
82
- if options[:field_index] == true
83
- attributes.reject { |_, value| value[:index] == false }
84
- else
83
+ if options[:field_index] == false
85
84
  attributes.select { |_, value| value[:index] == true }
85
+ else
86
+ attributes.reject { |_, value| value[:index] == false }
86
87
  end
87
88
  end
88
89
 
@@ -97,18 +98,38 @@ module Aliyun
97
98
  def update_index
98
99
  conf_res = Log.record_connection.get_index(project_name, logstore_name)
99
100
  raw_conf = JSON.parse(conf_res)
100
- index_conf = raw_conf.dup
101
- field_indices.each do |k, v|
102
- index_conf['keys'][k.to_s] ||= v
101
+ index_conf = raw_conf.deep_dup
102
+ field_index_types.each do |k, v|
103
+ index_conf['keys'][k.to_s] ||= {}
104
+ index_conf['keys'][k.to_s].merge!(v.as_json)
103
105
  end
104
106
  return if index_conf['keys'] == raw_conf['keys']
105
107
 
106
108
  Log.record_connection.update_index(
107
109
  project_name,
108
110
  logstore_name,
109
- index_conf['keys']
111
+ index_conf['keys'].with_indifferent_access
110
112
  )
111
113
  end
114
+
115
+ def field_index_types
116
+ field_indices.tap do |tap|
117
+ tap.each do |_, v|
118
+ v[:alias] ||= ''
119
+ v[:caseSensitive] ||= false
120
+ v[:chn] ||= false
121
+ v[:doc_value] = options[:field_doc_value] != false if v[:doc_value].nil?
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ def dump_attributes
128
+ attributes.dup.tap do |tap|
129
+ tap.each do |k, v|
130
+ tap[k] = TypeCasting.dump_field(v, self.class.attributes[k])
131
+ end
132
+ end
112
133
  end
113
134
 
114
135
  def save
@@ -116,7 +137,11 @@ module Aliyun
116
137
  run_callbacks(:create) do
117
138
  run_callbacks(:save) do
118
139
  if valid?
119
- res = Log.record_connection.put_log(self.class.project_name, self.class.logstore_name, attributes)
140
+ res = Log.record_connection.put_log(
141
+ self.class.project_name,
142
+ self.class.logstore_name,
143
+ dump_attributes
144
+ )
120
145
  res.code == 200
121
146
  else
122
147
  false
@@ -128,7 +153,7 @@ module Aliyun
128
153
  def save_array
129
154
  run_callbacks(:create) do
130
155
  run_callbacks(:save) do
131
- validate! && attributes
156
+ validate! && dump_attributes
132
157
  end
133
158
  end
134
159
  end
@@ -8,7 +8,63 @@ module Aliyun
8
8
  @klass = klass
9
9
  @opts = opts
10
10
  @klass.auto_load_schema
11
- @opts[:search] ||= '*'
11
+ end
12
+
13
+ def inspect
14
+ "#<#{self.class}>"
15
+ end
16
+
17
+ def first(line = 1)
18
+ find_offset(0, line, false)
19
+ end
20
+
21
+ def second
22
+ find_offset(1)
23
+ end
24
+
25
+ def third
26
+ find_offset(2)
27
+ end
28
+
29
+ def fourth
30
+ find_offset(3)
31
+ end
32
+
33
+ def fifth
34
+ find_offset(4)
35
+ end
36
+
37
+ def last(line = 1)
38
+ data = find_offset(0, line, true)
39
+ line > 1 ? data.reverse : data
40
+ end
41
+
42
+ def find_offset(nth, line = 1, reverse = false)
43
+ # @opts[:select] = '*'
44
+ @opts[:line] = line
45
+ @opts[:offset] = nth
46
+ if @opts[:order].present? && reverse
47
+ conds = []
48
+ @opts[:order].split(',').each do |field|
49
+ field.gsub!(/\s+desc|asc.*/i, '')
50
+ conds << "#{field.strip} DESC"
51
+ end
52
+ @opts[:order] = conds.join(', ')
53
+ end
54
+ @opts[:order] ||= reverse ? '__time__ DESC' : '__time__ ASC'
55
+ if @opts[:select].blank?
56
+ line <= 1 ? load[0] : load
57
+ else
58
+ line <= 1 ? result[0] : result
59
+ end
60
+ end
61
+
62
+ def scoping
63
+ previous = @klass.current_scope
64
+ @klass.current_scope = self
65
+ yield
66
+ ensure
67
+ @klass.current_scope = previous
12
68
  end
13
69
 
14
70
  def from(from)
@@ -36,21 +92,67 @@ module Aliyun
36
92
 
37
93
  def page(val)
38
94
  @opts[:page] = val - 1 if val >= 1
95
+ singleton_class.send(:define_method, :total_count) do
96
+ @total_count ||= count
97
+ end
98
+ singleton_class.send(:define_method, :total_pages) do
99
+ (total_count.to_f / (@opts[:line] || 100)).ceil
100
+ end
39
101
  self
40
102
  end
41
103
 
42
- def where(opts)
104
+ def api(opts)
43
105
  @opts.merge!(opts)
44
106
  self
45
107
  end
46
108
 
47
- def search(str)
48
- @opts[:search] = str
109
+ def search(*statement)
110
+ ql = statement_ql(*statement)
111
+ @opts[:search] = ql if ql.present?
49
112
  self
50
113
  end
51
114
 
52
- def sql(str)
53
- @opts[:sql] = str
115
+ def sql(*statement)
116
+ unless statement[0].is_a?(String)
117
+ raise ParseStatementInvalid, 'Only support string statement'
118
+ end
119
+ ql = sanitize_array(*statement)
120
+ @opts[:sql] = ql if ql.present?
121
+ self
122
+ end
123
+
124
+ def where(*statement)
125
+ if statement[0].is_a?(String)
126
+ ql = sanitize_array(*statement)
127
+ @opts[:where] = ql
128
+ else
129
+ ql = statement_ql(*statement)
130
+ @opts[:search] = ql
131
+ end
132
+ self
133
+ end
134
+
135
+ def select(*fields)
136
+ @opts[:select] = fields.join(', ')
137
+ self
138
+ end
139
+
140
+ def group(*fields)
141
+ @opts[:group] = fields.join(', ')
142
+ self
143
+ end
144
+
145
+ def order(*fields)
146
+ if fields[0].is_a?(Hash)
147
+ @opts[:order] = fields[0].map do |k, v|
148
+ unless %w[asc desc].include?(v.to_s)
149
+ raise ArgumentError, "Direction \"#{v}\" is invalid. Valid directions are: [:asc, :desc, :ASC, :DESC]"
150
+ end
151
+ "#{k} #{v}"
152
+ end.join(', ')
153
+ else
154
+ @opts[:order] = fields.join(', ')
155
+ end
54
156
  self
55
157
  end
56
158
 
@@ -60,36 +162,193 @@ module Aliyun
60
162
  end
61
163
 
62
164
  def count
165
+ # @opts[:select] = 'COUNT(*) as count'
166
+ _sql = to_sql
167
+ sql = "SELECT COUNT(*) as count"
168
+ sql += " FROM(#{_sql})" if _sql.present?
63
169
  query = @opts.dup
64
- if query[:query].blank?
65
- where_cond = query[:sql].split(/where /i)[1] if query[:sql].present?
66
- query[:query] = "#{query[:search]}|SELECT COUNT(*) as count"
67
- query[:query] = "#{query[:query]} WHERE #{where_cond}" if where_cond.present?
68
- end
69
- res = Log.record_connection.get_logs(@klass.project_name, @klass.logstore_name, query)
70
- res = JSON.parse(res.body)
71
- res[0]['count'].to_i
170
+ query[:query] = "#{query[:search] || '*'}|#{sql}"
171
+ res = execute(query)
172
+ res.dig(0, 'count').to_i
173
+ end
174
+
175
+ def sum(field)
176
+ @opts[:select] = "SUM(#{field}) as sum"
177
+ query = @opts.dup
178
+ query[:query] = "#{query[:search] || '*'}|#{to_sql}"
179
+ res = execute(query)
180
+ res.dig(0, 'sum').to_f
72
181
  end
73
182
 
74
183
  def result
75
184
  query = @opts.dup
76
- if query[:page]
77
- query[:line] ||= 100
78
- query[:offset] = query[:page] * query[:line]
185
+ query[:query] = query[:search] || '*'
186
+ sql = to_sql
187
+ if sql.present?
188
+ query[:query] += "|#{sql}"
189
+ @opts[:line] = nil
190
+ @opts[:offset] = nil
191
+ @opts[:page] = nil
79
192
  end
80
- query[:query] = query[:search]
81
- query[:query] = "#{query[:query]}|#{query[:sql]}" if query[:sql].present?
82
- res = Log.record_connection.get_logs(@klass.project_name, @klass.logstore_name, query)
83
- JSON.parse(res)
193
+ execute(query)
84
194
  end
85
195
 
86
196
  def load
87
197
  result.map do |json_attr|
88
- attrs = {}
89
- @klass.attributes.keys.each do |k, _|
90
- attrs[k] = json_attr[k.to_s]
198
+ record = @klass.new
199
+ json_attr.each do |key, value|
200
+ record.send("#{key}=", value) if record.respond_to?("#{key}=")
91
201
  end
92
- @klass.new(attrs)
202
+ record
203
+ end
204
+ end
205
+
206
+ def to_sql
207
+ opts = @opts.dup
208
+ sql_query = []
209
+ sql_query << "WHERE #{opts[:where]}" if opts[:where].present?
210
+ sql_query << "GROUP BY #{opts[:group]}" if opts[:group].present?
211
+ sql_query << "ORDER BY #{opts[:order]}" if opts[:order].present?
212
+ if sql_query.present? || opts[:select].present?
213
+ sql_query.insert(0, "SELECT #{opts[:select] || '*'}")
214
+ sql_query.insert(1, 'FROM log')
215
+ if opts[:line] || opts[:page] || opts[:offset]
216
+ parse_page
217
+ sql_query << "LIMIT #{@opts[:offset]},#{@opts[:line]}"
218
+ end
219
+ end
220
+ "#{opts[:query]}#{sql_query.join(' ')}"
221
+ end
222
+
223
+ private
224
+
225
+ def execute(query)
226
+ puts query.slice(:from, :to, :search, :query).to_json
227
+ res = Log.record_connection.get_logs(@klass.project_name, @klass.logstore_name, query)
228
+ JSON.parse(res)
229
+ end
230
+
231
+ def parse_page
232
+ @opts[:line] ||= 100
233
+ @opts[:page] ||= 0
234
+ @opts[:offset] = @opts[:page] * @opts[:line]
235
+ end
236
+
237
+ def statement_ql(*statement)
238
+ if statement.size == 1
239
+ sanitize_hash(statement.first)
240
+ elsif statement.size > 1
241
+ sanitize_array(*statement)
242
+ end
243
+ end
244
+
245
+ def sanitize_hash(search_content)
246
+ return search_content unless search_content.is_a?(Hash)
247
+
248
+ search_content.select { |_, v| v.present? }.map do |key, value|
249
+ options = @klass.attributes[:"#{key}"]
250
+ unless options
251
+ raise UnknownAttributeError, "unknown field '#{key}' for #{@klass.name}."
252
+ end
253
+
254
+ raise_if_hash_quote(value)
255
+
256
+ cast_type = options[:cast_type]
257
+ if value.is_a?(Array)
258
+ values = value.uniq.map { |v| _quote(cast_type, v) }
259
+ str_values = values.map { |v| "#{key}: #{v}" }.join(' OR ')
260
+ values.size > 1 ? "(#{str_values})" : str_values
261
+ elsif value.is_a?(Range)
262
+ "#{key} in [#{value.begin} #{value.end}]"
263
+ else
264
+ "#{key}: #{_quote(cast_type, value)}"
265
+ end
266
+ end.join(' AND ')
267
+ end
268
+
269
+ def sanitize_array(*ary)
270
+ statement, *values = ary
271
+ if values.first.is_a?(Hash) && /:\w+/.match?(statement)
272
+ replace_named_bind_variables(statement, values.first)
273
+ elsif statement.include?('?')
274
+ replace_bind_variables(statement, values)
275
+ elsif statement.blank? || values.blank?
276
+ statement
277
+ else
278
+ statement % values.collect(&:to_s)
279
+ end
280
+ end
281
+
282
+ def replace_named_bind_variables(statement, bind_vars)
283
+ statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match|
284
+ if bind_vars.include?(match = Regexp.last_match(2).to_sym)
285
+ match_value = bind_vars[match]
286
+ raise_if_hash_quote(match_value)
287
+ if match_value.is_a?(Array) || match_value.is_a?(Range)
288
+ values = match_value.map { |v| _quote_type_value(v) }
289
+ values.join(', ')
290
+ else
291
+ _quote_type_value(match_value)
292
+ end
293
+ else
294
+ raise ParseStatementInvalid, "missing value for :#{match} in #{statement}"
295
+ end
296
+ end
297
+ end
298
+
299
+ def replace_bind_variables(statement, values)
300
+ expected = statement.count('?')
301
+ provided = values.size
302
+ if expected != provided
303
+ raise ParseStatementInvalid, "wrong number of bind variables (#{provided} " \
304
+ "for #{expected}) in: #{statement}"
305
+ end
306
+ bound = values.dup
307
+ statement.gsub(/\?/) do
308
+ value = bound.shift
309
+ raise_if_hash_quote(value)
310
+ if value.is_a?(Array) || value.is_a?(Range)
311
+ values = value.map { |v| _quote_type_value(v) }
312
+ values.join(', ')
313
+ else
314
+ _quote_type_value(value)
315
+ end
316
+ end
317
+ end
318
+
319
+ def _quote(type, value)
320
+ v = TypeCasting.cast_field(value, cast_type: type || :string)
321
+ case type
322
+ when :string, nil then "'#{v.to_s}'"
323
+ when :bigdecimal then v.to_s("F")
324
+ when :integer then v.to_s.to_i
325
+ when :datetime, :date then "'#{v.iso8601}'"
326
+ else
327
+ value.to_s
328
+ end
329
+ end
330
+
331
+ def _quote_type_value(value)
332
+ case value.class.name
333
+ when 'String' then "'#{value.to_s}'"
334
+ when 'BigDecimal', 'Float' then value.to_s("F")
335
+ when 'Date', 'DateTime', 'Time' then "'#{value.iso8601}'"
336
+ else
337
+ value
338
+ end
339
+ end
340
+
341
+ def raise_if_hash_quote(value)
342
+ if value.is_a?(Hash) || value.is_a?(ActiveSupport::HashWithIndifferentAccess)
343
+ raise ParseStatementInvalid, "can't quote Hash"
344
+ end
345
+ end
346
+
347
+ def method_missing(method, *args, &block)
348
+ if @klass.respond_to?(method)
349
+ scoping { @klass.public_send(method, *args, &block) }
350
+ else
351
+ super
93
352
  end
94
353
  end
95
354
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aliyun
4
+ module Log
5
+ module PerThreadRegistry
6
+ def self.extended(object)
7
+ object.instance_variable_set '@per_thread_registry_key', object.name.freeze
8
+ end
9
+
10
+ def instance
11
+ Thread.current[@per_thread_registry_key] ||= new
12
+ end
13
+
14
+ private
15
+
16
+ def method_missing(name, *args, &block)
17
+ singleton_class.delegate name, to: :instance
18
+
19
+ send(name, *args, &block)
20
+ end
21
+ end
22
+
23
+ class ScopeRegistry
24
+ extend PerThreadRegistry
25
+
26
+ def initialize
27
+ @registry = Hash.new { |hash, key| hash[key] = {} }
28
+ end
29
+
30
+ def value_for(scope_type, model)
31
+ @registry[scope_type][model.name]
32
+ end
33
+
34
+ # Sets the +value+ for a given +scope_type+ and +model+.
35
+ def set_value_for(scope_type, model, value)
36
+ @registry[scope_type][model.name] = value
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'scope_registry'
4
+ require_relative 'relation'
5
+
6
+ module Aliyun
7
+ module Log
8
+ module Record
9
+ module Scoping
10
+ extend ActiveSupport::Concern
11
+
12
+ module ClassMethods
13
+ delegate :load, :result, :count, :sum, to: :all
14
+ delegate :where, :query, :search, :sql, :from, :to, :api,
15
+ :page, :line, :limit, :offset, :select, :group,
16
+ :order, :to_sql, to: :all
17
+ delegate :first, :last, :second, :third, :fourth, :fifth, :find_offset, to: :all
18
+
19
+ def current_scope
20
+ ScopeRegistry.value_for(:current_scope, self)
21
+ end
22
+
23
+ def current_scope=(scope)
24
+ ScopeRegistry.set_value_for(:current_scope, self, scope)
25
+ end
26
+
27
+ def scope(name, body)
28
+ raise ArgumentError, 'The scope body needs to be callable.' unless body.respond_to?(:call)
29
+
30
+ singleton_class.send(:define_method, name) do |*args|
31
+ scope = all
32
+ scope = scope.scoping { body.call(*args) }
33
+ scope
34
+ end
35
+ end
36
+
37
+ def unscoped
38
+ block_given? ? relation.scoping { yield } : relation
39
+ end
40
+
41
+ def all
42
+ scope = current_scope
43
+ scope ||= relation.from(0).to(Time.now.to_i)
44
+ scope
45
+ end
46
+
47
+ private
48
+
49
+ def relation
50
+ Relation.new(self)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Aliyun
6
+ module Log
7
+ module Record
8
+ module TypeCasting
9
+ TYPE_MAPPING = {
10
+ text: :string,
11
+ long: :integer,
12
+ double: :float,
13
+ json: :json
14
+ }.freeze
15
+
16
+ def self.cast_field(value, options)
17
+ options ||= {}
18
+ type = options[:cast_type]
19
+ type ||= TYPE_MAPPING[options[:type]]
20
+
21
+ return value if options.nil?
22
+ return nil if value.nil?
23
+
24
+ caster = Registry.lookup(type)
25
+ raise ArgumentError, "Unknown type #{options[:type]}" if caster.nil?
26
+
27
+ caster.new(options).cast(value)
28
+ end
29
+
30
+ def self.dump_field(value, options)
31
+ options ||= {}
32
+ type = options[:cast_type]
33
+ type ||= TYPE_MAPPING[options[:type]]
34
+
35
+ return value if options.nil?
36
+ return nil if value.nil?
37
+
38
+ dumper = Registry.lookup(type)
39
+ raise ArgumentError, "Unknown type #{options[:type]}" if dumper.nil?
40
+
41
+ dumper.new(options).dump(value)
42
+ end
43
+
44
+ class Value
45
+ def initialize(options)
46
+ @options = options
47
+ end
48
+
49
+ def cast(value)
50
+ value
51
+ end
52
+
53
+ def dump(value)
54
+ value
55
+ end
56
+ end
57
+
58
+ class StringType < Value; end
59
+
60
+ class DateType < Value
61
+ def cast(value)
62
+ return nil unless value.respond_to?(:to_date)
63
+
64
+ value.to_date
65
+ end
66
+
67
+ def dump(value)
68
+ if value.respond_to?(:to_date)
69
+ value.to_date.to_s
70
+ else
71
+ value.to_s
72
+ end
73
+ end
74
+ end
75
+
76
+ class DateTimeType < Value
77
+ def cast(value)
78
+ return nil unless value.respond_to?(:to_datetime)
79
+
80
+ dt = begin
81
+ ::DateTime.parse(value)
82
+ rescue StandardError
83
+ nil
84
+ end
85
+ if dt
86
+ seconds = string_utc_offset(value) || 0
87
+ offset = seconds_to_offset(seconds)
88
+ ::DateTime.new(dt.year, dt.mon, dt.mday, dt.hour, dt.min, dt.sec, offset)
89
+ else
90
+ value.to_datetime
91
+ end
92
+ end
93
+
94
+ def dump(value)
95
+ if value.respond_to?(:to_datetime)
96
+ value.to_datetime.iso8601
97
+ else
98
+ value.to_s
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def string_utc_offset(string)
105
+ Date._parse(string)[:offset]
106
+ end
107
+
108
+ def seconds_to_offset(seconds)
109
+ ActiveSupport::TimeZone.seconds_to_utc_offset(seconds)
110
+ end
111
+ end
112
+
113
+ class IntegerType < Value
114
+ def cast(value)
115
+ if value == true
116
+ 1
117
+ elsif value == false
118
+ 0
119
+ elsif value.is_a?(String) && value.blank?
120
+ nil
121
+ elsif value.is_a?(Float) && !value.finite?
122
+ nil
123
+ elsif !value.respond_to?(:to_i)
124
+ nil
125
+ else
126
+ value.to_i
127
+ end
128
+ end
129
+ end
130
+
131
+ class BigDecimalType < Value
132
+ def cast(value)
133
+ if value == true
134
+ 1
135
+ elsif value == false
136
+ 0
137
+ elsif value.is_a?(Symbol)
138
+ value.to_s.to_d
139
+ elsif value.is_a?(String) && value.blank?
140
+ nil
141
+ elsif value.is_a?(Float) && !value.finite?
142
+ nil
143
+ elsif !value.respond_to?(:to_d)
144
+ nil
145
+ else
146
+ value.to_d
147
+ end
148
+ end
149
+ end
150
+
151
+ class FloatType < Value
152
+ def cast(value)
153
+ if value == true
154
+ 1
155
+ elsif value == false
156
+ 0
157
+ elsif value.is_a?(Symbol)
158
+ value.to_s.to_f
159
+ elsif value.is_a?(String) && value.blank?
160
+ nil
161
+ elsif value.is_a?(Float) && !value.finite?
162
+ nil
163
+ elsif !value.respond_to?(:to_f)
164
+ nil
165
+ else
166
+ value.to_f
167
+ end
168
+ end
169
+ end
170
+
171
+ class JsonType < Value
172
+ def cast(value)
173
+ return value unless value.is_a?(String)
174
+
175
+ begin
176
+ ActiveSupport::JSON.decode(value)
177
+ rescue StandardError
178
+ nil
179
+ end
180
+ end
181
+
182
+ def dump(value)
183
+ ActiveSupport::JSON.encode(value) unless value.nil?
184
+ end
185
+ end
186
+
187
+ module Registry
188
+ module_function
189
+
190
+ @registrations = {}
191
+
192
+ def register(name, klass = nil)
193
+ @registrations[name.to_sym] = klass
194
+ end
195
+
196
+ def lookup(name)
197
+ name ||= :string
198
+ @registrations[name.to_sym]
199
+ end
200
+
201
+ register(:string, StringType)
202
+ register(:date, DateType)
203
+ register(:datetime, DateTimeType)
204
+ register(:bigdecimal, BigDecimalType)
205
+ register(:integer, IntegerType)
206
+ register(:float, FloatType)
207
+ register(:json, JsonType)
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
@@ -83,7 +83,7 @@ module Aliyun
83
83
  end
84
84
 
85
85
  logger.debug("Received HTTP response, code: #{response.code}, headers: " \
86
- "#{response.headers}, body: #{response.body}")
86
+ "#{response.headers}, body: #{response.body.force_encoding('UTF-8')}")
87
87
 
88
88
  response
89
89
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Aliyun
4
4
  module Log
5
- VERSION = '0.2.2'
5
+ VERSION = '0.2.11'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aliyun-log
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yingce Liu
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-09 00:00:00.000000000 Z
11
+ date: 2020-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -128,6 +128,7 @@ files:
128
128
  - lib/aliyun/log/common/logging.rb
129
129
  - lib/aliyun/log/config.rb
130
130
  - lib/aliyun/log/logstore.rb
131
+ - lib/aliyun/log/logtail.rb
131
132
  - lib/aliyun/log/project.rb
132
133
  - lib/aliyun/log/protobuf.rb
133
134
  - lib/aliyun/log/protocol.rb
@@ -136,6 +137,9 @@ files:
136
137
  - lib/aliyun/log/record/field.rb
137
138
  - lib/aliyun/log/record/persistence.rb
138
139
  - lib/aliyun/log/record/relation.rb
140
+ - lib/aliyun/log/record/scope_registry.rb
141
+ - lib/aliyun/log/record/scoping.rb
142
+ - lib/aliyun/log/record/type_casting.rb
139
143
  - lib/aliyun/log/request.rb
140
144
  - lib/aliyun/log/server_error.rb
141
145
  - lib/aliyun/version.rb