logstash-output-loki 1.1.0 → 1.2.0

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: 5c6817d07ada63cde10011f1e490f803ddbb0a7a54d75b9472780f4f180c65fe
4
- data.tar.gz: e457589a713ed3bc0f74120f3671bfb6b9b9760393cb86c18764e4d5a1fa2bd7
3
+ metadata.gz: a2345ce18e6ef9aad96401c5b7afb5dca4f5b7dcc71c7cf8a21a1b28496e78fc
4
+ data.tar.gz: 3fedc6382bd23200cf2f9afda2e6ec4d0222e72921ccee4563671af72250032a
5
5
  SHA512:
6
- metadata.gz: 358e07d5c964179b7a003e6ac840bb83df74c03e4de0f1916da310ed07e1c669a672d9363afd841c6bdfd49bb1d525ffbc815801ba4f3a8536659dd449173012
7
- data.tar.gz: 89360e4ffcb276b9406c017c5c019c43f839398a93a0cda46e0761e45a6a851ef2e9250c9fec5ba409062aef1f12576c9fcaa939a4cba0c0c9d0db15fa698f24
6
+ metadata.gz: ea1a970fe497c812132c999dbf73638c6baa20021b4bac22bb20114c525910d963eef3621ec5b69a3740732668de88ad4c9c09fa9a5569e7008030685d67b337
7
+ data.tar.gz: d03d649be3e5ed202f5e27aeb307e5db39c036b6c089748a982482d1b23df3e46c1563e61c20b081bdca7d62699b871e14ee569269a77d6d00760f23e70ea12d
@@ -52,7 +52,19 @@ module Loki
52
52
  def build_stream(stream)
53
53
  values = []
54
54
  stream['entries'].each { |entry|
55
- values.append([entry['ts'].to_s, entry['line']])
55
+ if entry.key?('metadata')
56
+ sorted_metadata = entry['metadata'].sort.to_h
57
+ values.append([
58
+ entry['ts'].to_s,
59
+ entry['line'],
60
+ sorted_metadata
61
+ ])
62
+ else
63
+ values.append([
64
+ entry['ts'].to_s,
65
+ entry['line']
66
+ ])
67
+ end
56
68
  }
57
69
  return {
58
70
  'stream'=>stream['labels'],
@@ -5,7 +5,7 @@ module Loki
5
5
  class Entry
6
6
  include Loki
7
7
  attr_reader :labels, :entry
8
- def initialize(event,message_field,include_fields)
8
+ def initialize(event,message_field,include_fields,metadata_fields)
9
9
  @entry = {
10
10
  "ts" => to_ns(event.get("@timestamp")),
11
11
  "line" => event.get(message_field).to_s
@@ -21,6 +21,22 @@ module Loki
21
21
  next if include_fields.length() > 0 and not include_fields.include?(key)
22
22
  @labels[key] = value.to_s
23
23
  }
24
+
25
+ # Unlike include_fields we should skip if no metadata_fields provided
26
+ if metadata_fields.length() > 0
27
+ @metadata = {}
28
+ event.to_hash.each { |key,value|
29
+ next if key.start_with?('@')
30
+ next if value.is_a?(Hash)
31
+ next if metadata_fields.length() > 0 and not metadata_fields.include?(key)
32
+ @metadata[key] = value.to_s
33
+ }
34
+
35
+ # Add @metadata to @entry if there was a match
36
+ if @metadata.size > 0
37
+ @entry.merge!('metadata' => @metadata)
38
+ end
39
+ end
24
40
  end
25
41
  end
26
42
  end
@@ -50,6 +50,9 @@ class LogStash::Outputs::Loki < LogStash::Outputs::Base
50
50
  ## 'An array of fields to map to labels, if defined only fields in this list will be mapped.'
51
51
  config :include_fields, :validate => :array, :default => [], :required => false
52
52
 
53
+ ## 'An array of fields to map to structure metadata, if defined only fields in this list will be mapped.'
54
+ config :metadata_fields, :validate => :array, :default => [], :required => false
55
+
53
56
  ## 'Backoff configuration. Maximum backoff time between retries. Default 300s'
54
57
  config :max_delay, :validate => :number, :default => 300, :required => false
55
58
 
@@ -71,7 +74,7 @@ class LogStash::Outputs::Loki < LogStash::Outputs::Base
71
74
  @logger.info("Loki output plugin", :class => self.class.name)
72
75
 
73
76
  # initialize Queue and Mutex
74
- @entries = Queue.new
77
+ @entries = Queue.new
75
78
  @mutex = Mutex.new
76
79
  @stop = false
77
80
 
@@ -94,7 +97,7 @@ class LogStash::Outputs::Loki < LogStash::Outputs::Base
94
97
  @mutex.synchronize do
95
98
  return if @stop
96
99
  end
97
-
100
+
98
101
  e = @entries.deq
99
102
  return if e.nil?
100
103
 
@@ -201,13 +204,13 @@ class LogStash::Outputs::Loki < LogStash::Outputs::Base
201
204
  ## Receives logstash events
202
205
  public
203
206
  def receive(event)
204
- @entries << Entry.new(event, @message_field, @include_fields)
207
+ @entries << Entry.new(event, @message_field, @include_fields, @metadata_fields)
205
208
  end
206
209
 
207
210
  def close
208
211
  @entries.close
209
- @mutex.synchronize do
210
- @stop = true
212
+ @mutex.synchronize do
213
+ @stop = true
211
214
  end
212
215
  @batch_wait_thread.join
213
216
  @batch_size_thread.join
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'logstash-output-loki'
3
- s.version = '1.1.0'
3
+ s.version = '1.2.0'
4
4
  s.authors = ['Aditya C S','Cyril Tovena']
5
5
  s.email = ['aditya.gnu@gmail.com','cyril.tovena@grafana.com']
6
6
 
@@ -21,31 +21,40 @@ describe Loki::Entry do
21
21
  {'@path' => '/path/to/file.log'},
22
22
  },
23
23
  'host' => '172.0.0.1',
24
+ 'trace_id' => 'trace_001',
24
25
  '@timestamp' => Time.now
25
26
  }
26
27
  )
27
28
  }
28
29
 
29
30
  it 'labels extracted should not contains object and metadata or timestamp' do
30
- entry = Entry.new(event,"message", [])
31
- expect(entry.labels).to eql({ 'agent' => 'filebeat', 'host' => '172.0.0.1', 'foo'=>'5'})
31
+ entry = Entry.new(event,"message", [], [])
32
+ expect(entry.labels).to eql({ 'agent' => 'filebeat', 'host' => '172.0.0.1', 'foo'=>'5', 'trace_id'=>'trace_001'})
32
33
  expect(entry.entry['ts']).to eql to_ns(event.get("@timestamp"))
33
34
  expect(entry.entry['line']).to eql 'hello'
34
35
  end
35
36
 
36
37
  it 'labels extracted should only contain allowlisted labels' do
37
- entry = Entry.new(event, "message", %w[agent foo])
38
+ entry = Entry.new(event, "message", %w[agent foo], [])
38
39
  expect(entry.labels).to eql({ 'agent' => 'filebeat', 'foo'=>'5'})
39
40
  expect(entry.entry['ts']).to eql to_ns(event.get("@timestamp"))
40
41
  expect(entry.entry['line']).to eql 'hello'
41
42
  end
43
+
44
+ it 'labels and structured metadata extracted should only contain allow listed labels and metadata' do
45
+ entry = Entry.new(event, "message", %w[agent foo], %w[trace_id])
46
+ expect(entry.labels).to eql({ 'agent' => 'filebeat', 'foo'=>'5'})
47
+ expect(entry.entry['ts']).to eql to_ns(event.get("@timestamp"))
48
+ expect(entry.entry['line']).to eql 'hello'
49
+ expect(entry.entry['metadata']).to eql({'trace_id' => 'trace_001'})
50
+ end
42
51
  end
43
52
 
44
53
  context 'test batch generation with label order' do
45
54
  let (:entries) {[
46
- Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","@timestamp"=>Time.at(1)}),"message", []),
47
- Entry.new(LogStash::Event.new({"log"=>"foobar","bar"=>"bar","@timestamp"=>Time.at(2)}),"log", []),
48
- Entry.new(LogStash::Event.new({"cluster"=>"us-central1","message"=>"foobuzz","buzz"=>"bar","@timestamp"=>Time.at(3)}),"message", []),
55
+ Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","@timestamp"=>Time.at(1)}),"message", [], []),
56
+ Entry.new(LogStash::Event.new({"log"=>"foobar","bar"=>"bar","@timestamp"=>Time.at(2)}),"log", [], []),
57
+ Entry.new(LogStash::Event.new({"cluster"=>"us-central1","message"=>"foobuzz","buzz"=>"bar","@timestamp"=>Time.at(3)}),"message", [], []),
49
58
 
50
59
  ]}
51
60
  let (:expected) {
@@ -28,15 +28,15 @@ describe LogStash::Outputs::Loki do
28
28
 
29
29
  context 'when adding en entry to the batch' do
30
30
  let (:simple_loki_config) {{'url' => 'http://localhost:3100'}}
31
- let (:entry) {Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","@timestamp"=>Time.at(1)}),"message", [])}
31
+ let (:entry) {Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","@timestamp"=>Time.at(1)}),"message", [], [])}
32
32
  let (:lbs) {{"buzz"=>"bar","cluster"=>"us-central1"}.sort.to_h}
33
33
  let (:include_loki_config) {{ 'url' => 'http://localhost:3100', 'include_fields' => ["cluster"] }}
34
- let (:include_entry) {Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","@timestamp"=>Time.at(1)}),"message", ["cluster"])}
34
+ let (:include_entry) {Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","@timestamp"=>Time.at(1)}),"message", ["cluster"], [])}
35
35
  let (:include_lbs) {{"cluster"=>"us-central1"}.sort.to_h}
36
36
 
37
37
  it 'should not add empty line' do
38
38
  plugin = LogStash::Plugin.lookup("output", "loki").new(simple_loki_config)
39
- emptyEntry = Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","@timestamp"=>Time.at(1)}),"foo", [])
39
+ emptyEntry = Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","@timestamp"=>Time.at(1)}),"foo", [], [])
40
40
  expect(plugin.add_entry_to_batch(emptyEntry)).to eql true
41
41
  expect(plugin.batch).to eql nil
42
42
  end
@@ -83,8 +83,51 @@ describe LogStash::Outputs::Loki do
83
83
  end
84
84
  end
85
85
 
86
+ context 'when building json from batch to send' do
87
+ let (:basic_loki_config) {{'url' => 'http://localhost:3100'}}
88
+ let (:basic_entry) {Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","trace_id"=>"trace_001","@timestamp"=>Time.at(1)}),"message", [], [])}
89
+ let (:include_loki_config) {{ 'url' => 'http://localhost:3100', 'include_fields' => ["cluster"] }}
90
+ let (:include_entry) {Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","trace_id"=>"trace_001","@timestamp"=>Time.at(1)}),"message", ["cluster"], [])}
91
+ let (:metadata_loki_config) {{ 'url' => 'http://localhost:3100', 'include_fields' => ["cluster"], 'metadata_fields' => ["trace_id"] }}
92
+ let (:metadata_entry) {Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","trace_id"=>"trace_001","@timestamp"=>Time.at(1)}),"message", ["cluster"], ["trace_id"])}
93
+ let (:metadata_multi_loki_config) {{ 'url' => 'http://localhost:3100', 'include_fields' => ["cluster"], 'metadata_fields' => ["trace_id", "user_id"] }}
94
+ let (:metadata_multi_entry) {Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","trace_id"=>"trace_001","user_id"=>"user_001","@timestamp"=>Time.at(1)}),"message", ["cluster"], ["trace_id", "user_id"])}
95
+
96
+ it 'should not include labels or metadata' do
97
+ plugin = LogStash::Plugin.lookup("output", "loki").new(basic_loki_config)
98
+ expect(plugin.batch).to eql nil
99
+ expect(plugin.add_entry_to_batch(basic_entry)).to eql true
100
+ expect(plugin.batch).not_to be_nil
101
+ expect(plugin.batch.to_json).to eq '{"streams":[{"stream":{"buzz":"bar","cluster":"us-central1","trace_id":"trace_001"},"values":[["1000000000","foobuzz"]]}]}'
102
+ end
103
+
104
+ it 'should include metadata with no labels' do
105
+ plugin = LogStash::Plugin.lookup("output", "loki").new(metadata_loki_config)
106
+ expect(plugin.batch).to eql nil
107
+ expect(plugin.add_entry_to_batch(metadata_entry)).to eql true
108
+ expect(plugin.batch).not_to be_nil
109
+ expect(plugin.batch.to_json).to eq '{"streams":[{"stream":{"cluster":"us-central1"},"values":[["1000000000","foobuzz",{"trace_id":"trace_001"}]]}]}'
110
+ end
111
+
112
+ it 'should include labels with no metadata' do
113
+ plugin = LogStash::Plugin.lookup("output", "loki").new(include_loki_config)
114
+ expect(plugin.batch).to eql nil
115
+ expect(plugin.add_entry_to_batch(include_entry)).to eql true
116
+ expect(plugin.batch).not_to be_nil
117
+ expect(plugin.batch.to_json).to eq '{"streams":[{"stream":{"cluster":"us-central1"},"values":[["1000000000","foobuzz"]]}]}'
118
+ end
119
+
120
+ it 'should include labels with multiple metadata' do
121
+ plugin = LogStash::Plugin.lookup("output", "loki").new(metadata_multi_loki_config)
122
+ expect(plugin.batch).to eql nil
123
+ expect(plugin.add_entry_to_batch(metadata_multi_entry)).to eql true
124
+ expect(plugin.batch).not_to be_nil
125
+ expect(plugin.batch.to_json).to eq '{"streams":[{"stream":{"cluster":"us-central1"},"values":[["1000000000","foobuzz",{"trace_id":"trace_001","user_id":"user_001"}]]}]}'
126
+ end
127
+ end
128
+
86
129
  context 'batch expiration' do
87
- let (:entry) {Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","@timestamp"=>Time.at(1)}),"message", [])}
130
+ let (:entry) {Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","@timestamp"=>Time.at(1)}),"message", [], [])}
88
131
 
89
132
  it 'should not expire if empty' do
90
133
  loki = LogStash::Outputs::Loki.new(simple_loki_config.merge!({'batch_wait'=>0.5}))
@@ -147,13 +190,13 @@ describe LogStash::Outputs::Loki do
147
190
  loki.receive(event)
148
191
  sent.deq
149
192
  sleep(0.01) # Adding a minimal sleep. In few cases @batch=nil might happen after evaluating for nil
150
- expect(loki.batch).to be_nil
193
+ expect(loki.batch).to be_nil
151
194
  loki.close
152
195
  end
153
196
  end
154
197
 
155
198
  context 'http requests' do
156
- let (:entry) {Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","@timestamp"=>Time.at(1)}),"message", [])}
199
+ let (:entry) {Entry.new(LogStash::Event.new({"message"=>"foobuzz","buzz"=>"bar","cluster"=>"us-central1","@timestamp"=>Time.at(1)}),"message", [], [])}
157
200
 
158
201
  it 'should send credentials' do
159
202
  conf = {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-output-loki
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aditya C S
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-01-27 00:00:00.000000000 Z
12
+ date: 2024-09-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  requirement: !ruby/object:Gem::Requirement