fluent-plugin-oceanbase-logs 0.1.3 → 0.1.4

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: ff677af2c1882b27e86cde4cbfef8ef79039f45eae20e2a7c8eebd4c2fdbd98e
4
- data.tar.gz: 2fbe0571f3c1e1b840ab6714faff44e5a7396cad011c4845fb710964db9a490b
3
+ metadata.gz: 8b21a8c2fad6676c5f9b5b8f00cd849091b8c4077c745b2fa7a462995f9b7b07
4
+ data.tar.gz: 0f1a70d45968dc28815fb3c507e85b7115cc6b3682f0a5a762a0bdc5467f9019
5
5
  SHA512:
6
- metadata.gz: 380099690cb075af74c720a6eb147fdb5a1234b428bac1d54e1ea3a36d191f2b47b2a7962ed97ee7f5f2c9f576aad55a4cf025bba39bd8152747fdc6439242bf
7
- data.tar.gz: 36cd5b13ca5fd22159451bc2c15c45c7861ad1122a62d9e95a4184c2e5e0a5f49c08183609e7767125d65330b69adf506060b9c59b228f785b641c56c41add20
6
+ metadata.gz: 02b0aaf81f118afdcc215f377f7c13ca2ca63d2a1cd28008f778b902e9277b614f28464973fbec4d5ce4a7550fbc3eca9a9f43833367379f58a7f967398f1a05
7
+ data.tar.gz: 2dfdd6d0bbe2ac20532ef99e1ef1e764d7d8fa9244043865370e9b1c77378766503ae19680f50266085e39a0ad8af0784ee00eeb19f904e4ee6523de902692f8
data/README.md CHANGED
@@ -44,6 +44,47 @@ tenant_id "#{ENV['OCEANBASE_TENANT_ID']}"
44
44
 
45
45
  Optional: `OCEANBASE_ENDPOINT`, `OCEANBASE_FETCH_INTERVAL`, `OCEANBASE_LOOKBACK_SECONDS`, `OCEANBASE_DB_NAME`, `OCEANBASE_SEARCH_KEYWORD`, `OCEANBASE_PROJECT_ID` — see `.env.example` and the Docker table below.
46
46
 
47
+ ### Multiple instances, tenants, and databases
48
+
49
+ Use one or more `<target>` blocks. Each block sets **`instance_id`** and **`tenant_id`** (required). Optional **`db_name`** applies the same `dbName` filter as the top-level parameter, for that scope only.
50
+
51
+ When any `<target>` is present, the plugin **only** uses those scopes; top-level `instance_id` / `tenant_id` / `db_name` are ignored (you may leave them empty). Global options such as `access_key_id`, `log_type`, `search_keyword`, `endpoint`, and `fetch_interval` still apply to every target.
52
+
53
+ Example: two clusters, one tenant each, and a second scope that filters one database on another tenant:
54
+
55
+ ```conf
56
+ <source>
57
+ @type oceanbase_logs
58
+ tag oceanbase.slow_sql
59
+ log_type slow_sql
60
+ access_key_id "#{ENV['OCEANBASE_ACCESS_KEY_ID']}"
61
+ access_key_secret "#{ENV['OCEANBASE_ACCESS_KEY_SECRET']}"
62
+ instance_id ""
63
+ tenant_id ""
64
+ endpoint api-cloud-cn.oceanbase.com
65
+ fetch_interval 60
66
+ lookback_seconds 600
67
+ deduplicate true
68
+ include_metadata true
69
+ <target>
70
+ instance_id "#{ENV['OCEANBASE_INSTANCE_1']}"
71
+ tenant_id "#{ENV['OCEANBASE_TENANT_A']}"
72
+ </target>
73
+ <target>
74
+ instance_id "#{ENV['OCEANBASE_INSTANCE_2']}"
75
+ tenant_id "#{ENV['OCEANBASE_TENANT_B']}"
76
+ db_name "#{ENV['OCEANBASE_DB_ANALYTICS']}"
77
+ </target>
78
+ <storage>
79
+ @type local
80
+ persistent true
81
+ path /var/log/fluentd/slow_sql_seen
82
+ </storage>
83
+ </source>
84
+ ```
85
+
86
+ The same tenant with multiple databases needs one `<target>` per database (or a target without `db_name` to pull all DBs for that tenant, if your API usage allows omitting `dbName`).
87
+
47
88
  ### Example: Slow SQL → JSON file
48
89
 
49
90
  Full sample: [`example/fluentd.conf`](example/fluentd.conf).
@@ -13,6 +13,8 @@ module Fluent::Plugin
13
13
 
14
14
  helpers :thread, :storage
15
15
 
16
+ FetchScope = Struct.new(:instance_id, :tenant_id, :db_name)
17
+
16
18
  DEFAULT_STORAGE_TYPE = 'local'
17
19
 
18
20
  LOG_TYPE_PATHS = {
@@ -28,10 +30,19 @@ module Fluent::Plugin
28
30
  config_param :access_key_secret, :string, secret: true,
29
31
  desc: "OceanBase Cloud AccessKey Secret."
30
32
 
31
- config_param :instance_id, :string,
32
- desc: "OceanBase cluster instance ID."
33
- config_param :tenant_id, :string,
34
- desc: "OceanBase tenant ID."
33
+ config_param :instance_id, :string, default: '',
34
+ desc: "OceanBase cluster instance ID (legacy single scope; ignored when <target> sections are present)."
35
+ config_param :tenant_id, :string, default: '',
36
+ desc: "OceanBase tenant ID (legacy single scope; ignored when <target> sections are present)."
37
+
38
+ config_section :target, param_name: :targets, multi: true, required: false do
39
+ config_param :instance_id, :string,
40
+ desc: "Instance ID for this scope."
41
+ config_param :tenant_id, :string,
42
+ desc: "Tenant ID for this scope."
43
+ config_param :db_name, :string, default: nil,
44
+ desc: "Optional database name filter for this scope."
45
+ end
35
46
  config_param :project_id, :string, default: nil,
36
47
  desc: "OceanBase Cloud project ID (X-Ob-Project-Id header)."
37
48
 
@@ -82,13 +93,14 @@ module Fluent::Plugin
82
93
  end
83
94
  raise Fluent::ConfigError, 'access_key_id is required and cannot be empty' if @access_key_id.empty?
84
95
  raise Fluent::ConfigError, 'access_key_secret is required and cannot be empty' if @access_key_secret.empty?
85
- raise Fluent::ConfigError, 'instance_id is required and cannot be empty (e.g. set OCEANBASE_INSTANCE_ID)' if @instance_id.empty?
86
- raise Fluent::ConfigError, 'tenant_id is required and cannot be empty (e.g. set OCEANBASE_TENANT_ID)' if @tenant_id.empty?
87
96
 
88
97
  %i[@db_name @search_keyword @node_ip @filter_condition @project_id].each do |iv|
89
98
  v = instance_variable_get(iv)
90
99
  instance_variable_set(iv, nil) if v.is_a?(String) && v.strip.empty?
91
100
  end
101
+
102
+ @fetch_scopes = build_fetch_scopes
103
+
92
104
  @api_path_segment = LOG_TYPE_PATHS[@log_type.to_s]
93
105
  if @deduplicate
94
106
  @seen_storage = storage_create(
@@ -112,6 +124,36 @@ module Fluent::Plugin
112
124
 
113
125
  private
114
126
 
127
+ def build_fetch_scopes
128
+ scopes = []
129
+ if @targets && !@targets.empty?
130
+ @targets.each_with_index do |t, idx|
131
+ iid = t.instance_id.to_s.strip
132
+ tid = t.tenant_id.to_s.strip
133
+ raise Fluent::ConfigError, "target[#{idx}]: instance_id is required and cannot be empty" if iid.empty?
134
+ raise Fluent::ConfigError, "target[#{idx}]: tenant_id is required and cannot be empty" if tid.empty?
135
+ db = normalize_optional_db_name(t.db_name)
136
+ scopes << FetchScope.new(iid, tid, db)
137
+ end
138
+ else
139
+ raise Fluent::ConfigError, 'instance_id is required and cannot be empty (e.g. set OCEANBASE_INSTANCE_ID), or define one or more <target> sections' if @instance_id.empty?
140
+ raise Fluent::ConfigError, 'tenant_id is required and cannot be empty (e.g. set OCEANBASE_TENANT_ID), or define one or more <target> sections' if @tenant_id.empty?
141
+ scopes << FetchScope.new(@instance_id, @tenant_id, @db_name)
142
+ end
143
+ scopes
144
+ end
145
+
146
+ def normalize_optional_db_name(value)
147
+ return nil if value.nil?
148
+ s = value.to_s.strip
149
+ s.empty? ? nil : s
150
+ end
151
+
152
+ def dedup_storage_key(scope, dedup_id)
153
+ db = scope.db_name.to_s
154
+ "trace:#{scope.instance_id}:#{scope.tenant_id}:#{db}:#{dedup_id}"
155
+ end
156
+
115
157
  def run
116
158
  until @finished
117
159
  begin
@@ -136,24 +178,26 @@ module Fluent::Plugin
136
178
  now = Time.now.utc
137
179
  start_time = (now - @lookback_seconds).strftime('%Y-%m-%dT%H:%M:%SZ')
138
180
  end_time = now.strftime('%Y-%m-%dT%H:%M:%SZ')
139
- fetch_and_emit_samples(start_time, end_time)
181
+ @fetch_scopes.each do |scope|
182
+ fetch_and_emit_samples(start_time, end_time, scope)
183
+ end
140
184
  end
141
185
 
142
186
  # Fetch list then per-execution samples (one record per trace)
143
- def fetch_and_emit_samples(start_time, end_time)
144
- list_response = call_list_api(start_time, end_time)
187
+ def fetch_and_emit_samples(start_time, end_time, scope)
188
+ list_response = call_list_api(start_time, end_time, scope)
145
189
  return unless list_response
146
190
 
147
191
  sql_records = extract_records(list_response)
148
192
  return if sql_records.nil? || sql_records.empty?
149
193
 
150
194
  sql_ids = sql_records.map { |r| r['sqlId'] }.compact.uniq
151
- log.debug "Found #{sql_ids.size} unique SQL IDs, fetching samples..."
195
+ log.debug "Found #{sql_ids.size} unique SQL IDs for instance=#{scope.instance_id} tenant=#{scope.tenant_id} db=#{scope.db_name.inspect}, fetching samples..."
152
196
 
153
197
  total_emitted = 0
154
198
 
155
199
  sql_ids.each do |sql_id|
156
- samples = fetch_samples_for_sql(sql_id, start_time, end_time)
200
+ samples = fetch_samples_for_sql(sql_id, start_time, end_time, scope)
157
201
  next if samples.nil? || samples.empty?
158
202
 
159
203
  es = Fluent::MultiEventStream.new
@@ -163,13 +207,13 @@ module Fluent::Plugin
163
207
  dedup_id = trace_id || "#{sql_id}_#{sample['requestTime']}"
164
208
 
165
209
  if @deduplicate
166
- dedup_key = :"trace_#{dedup_id}"
210
+ dedup_key = dedup_storage_key(scope, dedup_id)
167
211
  next if @seen_storage.get(dedup_key)
168
212
  @seen_storage.put(dedup_key, Time.now.to_i.to_s)
169
213
  end
170
214
 
171
215
  sample['ob_log_type'] = @log_type.to_s
172
- sample = attach_metadata(sample, start_time, end_time) if @include_metadata
216
+ sample = attach_metadata(sample, start_time, end_time, scope) if @include_metadata
173
217
 
174
218
  event_time = if sample['requestTime']
175
219
  begin
@@ -190,26 +234,26 @@ module Fluent::Plugin
190
234
  end
191
235
  end
192
236
 
193
- log.info "Emitted #{total_emitted} #{@log_type} sample events (#{start_time} ~ #{end_time})" if total_emitted > 0
237
+ log.info "Emitted #{total_emitted} #{@log_type} sample events for instance=#{scope.instance_id} tenant=#{scope.tenant_id} (#{start_time} ~ #{end_time})" if total_emitted > 0
194
238
  end
195
239
 
196
- def fetch_samples_for_sql(sql_id, start_time, end_time)
197
- path = "/api/v2/instances/#{@instance_id}/tenants/#{@tenant_id}/sqls/#{sql_id}/samples"
240
+ def fetch_samples_for_sql(sql_id, start_time, end_time, scope)
241
+ path = "/api/v2/instances/#{scope.instance_id}/tenants/#{scope.tenant_id}/sqls/#{sql_id}/samples"
198
242
  params = {
199
243
  'startTime' => start_time,
200
244
  'endTime' => end_time,
201
245
  }
202
- params['dbName'] = @db_name if @db_name
246
+ params['dbName'] = scope.db_name if scope.db_name
203
247
 
204
248
  response = call_api_raw(path, params)
205
249
  return nil unless response
206
250
  extract_records(response)
207
251
  end
208
252
 
209
- def attach_metadata(record, start_time, end_time)
253
+ def attach_metadata(record, start_time, end_time, scope)
210
254
  record.merge(
211
- 'ob_instance_id' => @instance_id,
212
- 'ob_tenant_id' => @tenant_id,
255
+ 'ob_instance_id' => scope.instance_id,
256
+ 'ob_tenant_id' => scope.tenant_id,
213
257
  'query_start_time' => start_time,
214
258
  'query_end_time' => end_time
215
259
  )
@@ -235,13 +279,13 @@ module Fluent::Plugin
235
279
 
236
280
  # ---- API calls ----
237
281
 
238
- def call_list_api(start_time, end_time)
239
- path = "/api/v2/instances/#{@instance_id}/tenants/#{@tenant_id}/#{@api_path_segment}"
282
+ def call_list_api(start_time, end_time, scope)
283
+ path = "/api/v2/instances/#{scope.instance_id}/tenants/#{scope.tenant_id}/#{@api_path_segment}"
240
284
  params = {
241
285
  'startTime' => start_time,
242
286
  'endTime' => end_time,
243
287
  }
244
- params['dbName'] = @db_name if @db_name
288
+ params['dbName'] = scope.db_name if scope.db_name
245
289
  params['searchKeyWord'] = @search_keyword if @search_keyword
246
290
  params['nodeIp'] = @node_ip if @node_ip
247
291
  params['filterCondition'] = @filter_condition if @filter_condition
@@ -2,7 +2,7 @@ module Fluent
2
2
  module Plugin
3
3
  module OceanBase
4
4
  module Logs
5
- VERSION = "0.1.3"
5
+ VERSION = "0.1.4"
6
6
  end
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-oceanbase-logs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - OceanBase Integrations