franz 2.0.2 → 2.1.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
  SHA1:
3
- metadata.gz: 18c5e2e6c0b3f391583e4f98d594091363865650
4
- data.tar.gz: 31764ccf1465afbbc33ca45d198f85c839c20fdd
3
+ metadata.gz: bdc8e2f8a34ec53cc121ba4c3208bcff7c52b496
4
+ data.tar.gz: 8f70dbf552fa97e18f0c435ecf2f05b96bdfdf0d
5
5
  SHA512:
6
- metadata.gz: d132f1ebf38ca2691b12cb9e5143414b07ae2301254a6b8c7f608176818311e2ffc29a97600a1b935850b6b0f738e99a8e650257da59e2d1264b1c770dead8c2
7
- data.tar.gz: 757893332467aaeeb7eb9398534b817b981dea1a518e27e83b07b57add7fab505dc86bc1636309884d1e6b135aaed0ba6e9bddd926853273b967b67711bdfb73
6
+ metadata.gz: aeff8102b0d4635749bfc9751ee01e5dcddb1674635c5130d997800d561c6f955a55575deb015590d2be6586d3bec7ce7d5414504eeb469ec0b251887bffc9d7
7
+ data.tar.gz: 9d9dba062334ea71d64a2be833a061911d3215738e7ff762695e69369f12071600f0a16619f77fffd2472bf51e2d0ba875d28a0a37b2f0bcd4a05b81f53a3206
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.2
1
+ 2.1.0
@@ -20,6 +20,11 @@ module Franz
20
20
  # Turn here to strangle your dictator
21
21
  EMAIL = 'sclemmer@bluejeans.com'
22
22
 
23
+ # Bundled extensions
24
+ TRAVELING_RUBY_VERSION = '20150210-2.1.5'
25
+ SNAPPY_VERSION = '0.0.11'
26
+ EM_VERSION = '1.0.5'
27
+
23
28
  # Every project deserves its own ASCII art
24
29
  ART = <<-'EOART' % VERSION
25
30
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: franz
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Clemmer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-10 00:00:00.000000000 Z
11
+ date: 2015-03-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: slog
@@ -81,47 +81,47 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: 1.0.0
83
83
  - !ruby/object:Gem::Dependency
84
- name: eventmachine
84
+ name: poseidon
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - '='
87
+ - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 1.0.5
89
+ version: 0.0.5
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - '='
94
+ - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 1.0.5
96
+ version: 0.0.5
97
97
  - !ruby/object:Gem::Dependency
98
- name: poseidon
98
+ name: snappy
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - "~>"
101
+ - - '='
102
102
  - !ruby/object:Gem::Version
103
- version: 0.0.5
103
+ version: 0.0.11
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - "~>"
108
+ - - '='
109
109
  - !ruby/object:Gem::Version
110
- version: 0.0.5
110
+ version: 0.0.11
111
111
  - !ruby/object:Gem::Dependency
112
- name: snappy
112
+ name: eventmachine
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - "~>"
115
+ - - '='
116
116
  - !ruby/object:Gem::Version
117
- version: 0.0.11
117
+ version: 1.0.5
118
118
  type: :runtime
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - "~>"
122
+ - - '='
123
123
  - !ruby/object:Gem::Version
124
- version: 0.0.11
124
+ version: 1.0.5
125
125
  description: Aggregate log file events and send them elsewhere.
126
126
  email: sclemmer@bluejeans.com
127
127
  executables:
@@ -129,15 +129,10 @@ executables:
129
129
  extensions: []
130
130
  extra_rdoc_files: []
131
131
  files:
132
- - ".gitignore"
133
- - Gemfile
134
- - History.md
135
132
  - LICENSE
136
- - Rakefile
137
133
  - Readme.md
138
134
  - VERSION
139
135
  - bin/franz
140
- - franz.gemspec
141
136
  - lib/franz.rb
142
137
  - lib/franz/agg.rb
143
138
  - lib/franz/config.rb
data/.gitignore DELETED
@@ -1,14 +0,0 @@
1
- *~
2
- *.gem
3
- *.log
4
- *.out
5
- *.pid
6
- *.swp
7
- *.state
8
- *.checkpoint
9
- .DS_Store
10
- .yardoc
11
- doc
12
- pkg
13
- config.json
14
- Gemfile.lock
data/Gemfile DELETED
@@ -1,15 +0,0 @@
1
- source 'http://rubygems.org'
2
-
3
- gemspec
4
-
5
- group :development do
6
- gem 'pry'
7
- gem 'rake'
8
- gem 'yard'
9
- gem 'version'
10
- gem 'rubygems-tasks'
11
- end
12
-
13
- group :test do
14
- gem 'minitest'
15
- end
data/History.md DELETED
@@ -1,287 +0,0 @@
1
- # Franz
2
-
3
- Hi there, your old pal Sean Clemmer here. Imma talk quite a lot, so we might as
4
- well get acquainted. I work on the Operations team at Blue Jeans Network, an
5
- enterprise videoconferencing provider based in Mountain View, CA. Our team
6
- provides infrastructure, tools, and support to the Engineering team, including,
7
- most importantly for our purposes, log storage and processing.
8
-
9
-
10
- ## A lil History
11
-
12
- Before the latest rearchitecture, logs at Blue Jeans were basically `rsync`ed
13
- from every host to a central log server. Once on the box, a few processes came
14
- afterwards to compress the files and scan for meeting identifiers. Our reporting
15
- tools queried the log server with a meeting ID, and it replied with a list of
16
- files and their location.
17
-
18
- Compression saved a lot of space, and the search index was fairly small, since
19
- we only needed to store a map of meeting IDs to file paths. If you wanted to
20
- search the text, you need to log into the log server itself and `grep`.
21
-
22
- And all of that worked for everyone until a point. At a certain number of files,
23
- `grep` just wasn't fast enough, and worse, it was stealing resources necessary
24
- for processing logs. At a certain volume, we just couldn't scan the logs fast
25
- enough. Our scripts were getting harder to maintain, and we were looking for
26
- answers sooner rather than later.
27
-
28
-
29
- ### Exploring our options
30
-
31
- We did a fair amount of research and fiddling before deciding anything. We
32
- looked especially hard at the Elasticsearch-Logstash-Kibana (ELK) stack,
33
- Graylog2, and rearchitecting our scripts as a distributed system. In the end,
34
- we decided we weren't smart enough and there wasn't enough time to design our
35
- own system from the ground up. We also found Graylog2 to be a bit immature and
36
- lacking in features compared to the ELK stack.
37
-
38
- In the end, we appreciated the ELK stack had a lot of community and corporate
39
- support, it was easy to get started, and everything was fairly well-documented.
40
- Elasticsearch in particular seemed like a well-architected and professional
41
- software project. Logstash had an active development community, and the author
42
- Jordan Sissel soon joined Elasticsearch, Inc. as an employee.
43
-
44
- There was a lot of hype around ELK, and I thought we could make it work.
45
-
46
-
47
- ### Our requirements
48
-
49
- If you'll recall, the old system was really geared to look up log files based
50
- on a special meeting ID. Quick, but no real search.
51
-
52
- To emulate this with Elasticsearch we might store the whole file in a document
53
- along with the meeting ID, which would make queries straightforward. A more
54
- common approach in the ELK community is to store individual lines of a log file
55
- in Elasticsearch documents. I figured we could get the file back by asking the
56
- Elasticsearch cluster for an aggregation of documents corresponding to a given
57
- file path on a given host.
58
-
59
- To prove it to I slapped a couple scripts together, although the initial
60
- implementation actually used *facets* and not *aggregations*. If we could get
61
- the file back, that was everything we needed; we got advanced query support with
62
- Elasticsearch and visualization with Kibana for free. Logstash was making it
63
- easy for me to play around. Fun, even.
64
-
65
-
66
- ### Moving forward with ELK
67
-
68
- I got to reading about the pipelines, and I realized pretty quick we were't
69
- just gonna be able to hook Logstash right into Elasticsearch. You should put
70
- a kind of buffer inbetween, and both RabbitMQ and Redis were popular at the
71
- time. While I was developing our solution, alternative "forwarders" like
72
- Lumberjack were just being introduced. After evaluating our options, I decided
73
- on RabbitMQ based on team experience and the native Logstash support.
74
-
75
- So the initial pipeline looked like this:
76
-
77
- Logstash -> RabbitMQ -> Logstash -> Elasticsearch <- Kibana
78
-
79
- The first Logstash stage picked up logs with the `file` input and shipped them
80
- out with the `rabbitmq` output. These log *events* sat in RabbitMQ until a
81
- second, dedicated Logstash agent came along to parse it and shove it into
82
- Elasticsearch for long-term storage and search. I slapped Kibana on top to
83
- provide our users a usable window into the cluster.
84
-
85
- And it all *kinda* worked. It wasn't very fast, and outages were fairly common,
86
- but the all pieces were on the board. Over a few weeks I tuned and expanded the
87
- RabbitMQ and Elasticsearch clusters, but still we were missing chunks of files,
88
- missing whole files, and Logstash would die regularly with all kinds of strange
89
- issues. Encoding issues, buffer issues, timeout issues, heap size issues.
90
-
91
-
92
- ### Fighting with Logstash
93
-
94
- Surely we weren't the only people running into issues with this very popular
95
- piece of open source software? Logstash has a GitHub project, a JIRA account,
96
- and an active mailing list. I scoured issues, source code, pull requests, and
97
- e-mail archives. These seemed like huge bugs:
98
-
99
- 1. *Multiline:* Both the multiline codec and filter had their issues. The codec
100
- is generally preffered, but even still you might miss the last line of your
101
- file, because Logstash does not implement *multiline flush*. Logstash will
102
- buffer the last line of a file indefinitely, thinking you may come back and
103
- write to the log.
104
- 2. *File handling:* Because Logstash keeps files open indefinitely, it can soak
105
- up file handles after running a while. We had tens of thousands of log files
106
- on some hosts, and Logstash just wasn't having it.
107
- 3. *Reconstruction:* Despite my initial proofs, we were having a lot of trouble
108
- reconstructing log files from individual events. Lines were often missing,
109
- truncated, and shuffled.
110
-
111
- The multiline issue was actually fixed by the community, so I forked Logstash,
112
- applied some patches, and did a little hacking to get it working right.
113
-
114
- Fixing the second issue required delving deep into the depths of Logstash, the
115
- `file` input, and Jordan Sissel's FileWatch project. FileWatch provides most of
116
- the implementation for the `file` input, but it was riddled with bugs. I forked
117
- the project and went through a major refactor to simplify and sanitize the code.
118
- Eventually I was able to make it so Logstash would relinquish a file handle
119
- some short interval after reading the file had ceased.
120
-
121
- The third issue was rather more difficult. Subtle bugs at play. Rather than
122
- relying on the `@timestamp` field, which we found did not have enough
123
- resolution, I added a new field called `@seq`, just a simple counter, which
124
- enabled us to put the events back in order. Still we were missing chunks, and
125
- some lines appeared to be interleaved. Just weird stuff.
126
-
127
- After hacking Logstash half to death we decided the first stage of the pipeline
128
- would have to change. We'd still use Logstash to move events from RabbitMQ into
129
- Elasticsearch, but we couldn't trust it to collect files.
130
-
131
-
132
- ### And so Franz was born
133
-
134
- I researched Logstash alternatives, but there weren't many at the time. Fluentd
135
- looked promising, but early testing revealed the multiline facility wasn't quite
136
- there yet. Lumberjack was just gaining some popularity, but it was still too
137
- immature. In the end, I decided I had a pretty good handle on our requirements
138
- and I would take a stab at implementing a solution.
139
-
140
- It would be risky, but Logstash and the community just weren't moving fast
141
- enough for our needs. Engineering was justly upset with our logging "solution",
142
- and I was pretty frantic after weeks of hacking and debugging. How hard could
143
- it really be to tail a file and send the lines out to a queue?
144
-
145
- After a few prototypes and a couple false starts, we had our boy Franz.
146
-
147
-
148
-
149
- ## Design and Implementation
150
-
151
- From 10,000 feet Franz and Logstash are pretty similar; you can imagine Franz is
152
- basically a Logstash agent configured with a `file` input and `rabbitmq` output.
153
- Franz accepts a single configuration file that tells the process which files to
154
- tail, how to handle them, and where to send the output. Besides solving the
155
- three issues we discussed earlier, Franz provides a kind of `json` codec and
156
- `drop` filter (in Logstash parlance).
157
-
158
- I decided early on to implement Franz in Ruby, like Logstash. Unlike Logstash,
159
- which is typically executed by JRuby, I decided to stick with Mat'z Ruby for
160
- Franz in order to obtain a lower resource footprint at the expense of true
161
- concurrency (MRI has a GIL).
162
-
163
- Implementation-wise, Franz bears little resemblance to Logstash. Logstash has
164
- a clever system which "compiles" the inputs, filters, and outputs into a single
165
- block of code. Franz is a fairly straightward Ruby program with only a handful
166
- of classes and a simple execution path.
167
-
168
-
169
- ### The Twelve-Factor App
170
-
171
- I was heavily influenced by [the Twelve-Factor App](http://12factor.net):
172
-
173
- 1. *Codebase:* Franz is contained in a single repo on [GitHub](https://github.com/sczizzo/franz).
174
- 2. *Dependencies:* Franz provides a `Gemfile` to isolate dependencies.
175
- 3. *Config:* Franz separates code from configuration (no env vars, though).
176
- 4. *Backing Services:* Franz is agnostic to the connected RabbitMQ server.
177
- 5. *Build, release, run:* Franz is versioned and released as a [RubyGem](https://rubygems.org/gems/franz).
178
- 6. *Processes:* Franz provides mostly-stateless share-nothing executions.
179
- 7. *Port binding:* Franz isn't a Web service, so no worries here!
180
- 8. *Concurrency:* Franz is a single process and plays nice with Upstart.
181
- 9. *Disposability:* Franz uses a crash-only architecture, discussed below.
182
- 10. *Dev/prod parity:* We run the same configuration in every environment.
183
- 11. *Logs:* Franz provides structured logs which can be routed to file.
184
- 12. *Admin processes:* Franz is simple enough this isn't necessary.
185
-
186
-
187
- ### Crash-Only Architecture
188
-
189
- Logstash assumes you might want to stop the process and restart it later, having
190
- the new instance pick up where the last left off. To support this, Logstash (or
191
- really, FileWatch) keeps a small "checkpoint" file, which is written whenever
192
- Logstash is shut down.
193
-
194
- Franz takes this one step further and implements a ["crash-only" design](http://lwn.net/Articles/191059).
195
- The basic idea here the application does not distinguish between a crash and
196
- a restart. In practical terms, Franz simply writes a checkpoint at regular
197
- intervals; when asked to shut down, it aborts immediately.
198
-
199
- Franz checkpoints are simple, too. It's just a `Hash` from log file paths to
200
- the current `cursor` (byte offset) and `seq` (sequence number):
201
-
202
- {
203
- "/path/to/my.log": {
204
- "cursor": 1234,
205
- "seq": 99
206
- }
207
- }
208
-
209
- The checkpoint file contains the `Marshal` representation of this `Hash`.
210
-
211
-
212
- ### Sash
213
-
214
- The `Sash` is a data structure I discovered during the development of Franz,
215
- which came out of the implementation of multiline flush. Here's a taste:
216
-
217
- s = Sash.new # => #<Sash...>
218
- s.keys # => []
219
- s.insert :key, :value # => :value
220
- s.get :key # => [ :value ]
221
- s.insert :key, :crazy # => :crazy
222
- s.mtime :key # => 2014-02-18 21:24:30 -0800
223
- s.flush :key # => [ :value, :crazy ]
224
-
225
- For multilining, what you do is create a `Sash` keyed by each path, and insert
226
- each line in the appropriate key as they come in from upstream. Before you
227
- insert it, though, you check if the line in question matches the multiline
228
- pattern for the key: If so, you flush the `Sash` key and write the result out as
229
- an event. Now the `Sash` key will buffer the next event.
230
-
231
- In fact, a `Sash` key will only ever contain lines for at most one event, and
232
- the `mtime` method allows us to know how recently that key was modified. To
233
- implement multiline flush correctly, we periodically check the `Sash` for old
234
- keys and flush them according to configuration. `Sash` methods are threadsafe,
235
- so we can do this on the side without interrupting the main thread.
236
-
237
-
238
- ### Slog
239
-
240
- [Slog](https://github.com/sczizzo/slog) was also factored out of Franz, but
241
- it's heavily inspired by Logstash. By default, output is pretty and colored (I
242
- swear):
243
-
244
- Slog.new.info 'example'
245
- #
246
- # {
247
- # "level": "info",
248
- # "@timestamp": "2014-12-25T06:22:43.459-08:00",
249
- # "message": "example"
250
- # }
251
- #
252
-
253
- `Slog` works perfectly with Logstash or Franz when configured to treat the log
254
- file as JSON; they'll add the other fields necessary for reconstruction.
255
-
256
- More than anything, structured logging has changed how I approach logs. Instead
257
- of writing *everything* to file, Franz strives to log useful events that contain
258
- metadata. Instead of every request, an occasional digest. Instead of a paragraph
259
- of text, a simple summary. Franz uses different log levels appropriately,
260
- allowing end users to control verbosity.
261
-
262
-
263
- ### Execution Path
264
-
265
- Franz is implemented as a series of stages connected via bounded queues:
266
-
267
- Input -> Discover -> Watch -> Tail -> Agg -> Output
268
-
269
- Each of these stages is a class under the `Franz` namespace, and they run up
270
- to a couple `Thread`s, typically a worker and maybe a helper (e.g. multiline
271
- flush). Communicating via `SizedQueue`s helps ensure correctness and constrain
272
- memory usage under high load.
273
-
274
- 0. `Input`: Actually wires together `Discover`, `Watch`, `Tail`, and `Agg`.
275
- 1. `Discover`: Performs half of file existence detection by expanding globs and
276
- keeping track of files known to Franz.
277
- 2. `Watch`: Works in tandem with `Discover` to maintain a list of known files and
278
- their status. Events are generated when a file is created, destroyed, or
279
- modified (including appended, truncated, and replaced).
280
- 3. `Tail`: Receives low-level file events from a `Watch` and handles the actual
281
- reading of files, providing a stream of lines.
282
- 4. `Agg`: Mostly aggregates `Tail` events by applying the multiline filter, but it
283
- also applies the `host` and `type` fields. Basically, it does all the post
284
- processing after we've retreived a line from a file.
285
- 5. `Output`: RabbitMQ output for Franz, based on the really-very-good [Bunny](https://github.com/ruby-amqp/bunny)
286
- client. You must declare an `x-consistent-hash` exchange, as we generate a
287
- random `Integer` for routing. Such is life.
data/Rakefile DELETED
@@ -1,32 +0,0 @@
1
- require 'rubygems'
2
- require 'bundler'
3
- require 'rake'
4
-
5
-
6
- require 'rake/testtask'
7
- Rake::TestTask.new(:test) do |test|
8
- test.libs << 'lib' << 'test'
9
- test.test_files = FileList['test/test*.rb']
10
- test.verbose = true
11
- end
12
-
13
- task :default => :test
14
-
15
-
16
- require 'yard'
17
- YARD::Rake::YardocTask.new do |t|
18
- t.files = %w[ --readme Readme.md lib/**/*.rb - VERSION ]
19
- end
20
-
21
-
22
- require 'rubygems/tasks'
23
- Gem::Tasks.new({
24
- sign: {}
25
- }) do |tasks|
26
- tasks.console.command = 'pry'
27
- end
28
- Gem::Tasks::Sign::Checksum.new sha2: true
29
-
30
-
31
- require 'rake/version_task'
32
- Rake::VersionTask.new
data/franz.gemspec DELETED
@@ -1,29 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path(File.join('..', 'lib'), __FILE__)
3
- require 'franz/metadata'
4
-
5
- Gem::Specification.new do |s|
6
- s.name = 'franz'
7
- s.version = Franz::VERSION
8
- s.platform = Gem::Platform::RUBY
9
- s.license = Franz::LICENSE
10
- s.homepage = Franz::HOMEPAGE
11
- s.author = Franz::AUTHOR
12
- s.email = Franz::EMAIL
13
- s.summary = Franz::SUMMARY
14
- s.description = Franz::SUMMARY + '.'
15
-
16
- s.add_runtime_dependency 'slog', '~> 1.1.0'
17
- s.add_runtime_dependency 'bunny', '~> 1.6.0'
18
- s.add_runtime_dependency 'trollop', '~> 2.1.0'
19
- s.add_runtime_dependency 'colorize', '~> 0.7.0'
20
- s.add_runtime_dependency 'deep_merge', '~> 1.0.0'
21
- s.add_runtime_dependency 'eventmachine', '= 1.0.5'
22
- s.add_runtime_dependency 'poseidon', '~> 0.0.5'
23
- s.add_runtime_dependency 'snappy', '~> 0.0.11'
24
-
25
- s.files = `git ls-files`.split("\n")
26
- s.test_files = `git ls-files -- test/*`.split("\n")
27
- s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File::basename(f) }
28
- s.require_paths = %w[ lib ]
29
- end