logplex 0.0.6 → 1.0.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.
@@ -0,0 +1,28 @@
1
+ Layout/SpaceInsideHashLiteralBraces:
2
+ Enabled: true
3
+ EnforcedStyle: space
4
+ EnforcedStyleForEmptyBraces: no_space
5
+
6
+ Style/FrozenStringLiteralComment:
7
+ Enabled: true
8
+
9
+ Style/TrailingCommaInArguments:
10
+ Enabled: true
11
+ EnforcedStyleForMultiline: consistent_comma
12
+
13
+ Style/TrailingCommaInArrayLiteral:
14
+ Enabled: true
15
+ EnforcedStyleForMultiline: consistent_comma
16
+
17
+ Style/TrailingCommaInHashLiteral:
18
+ Enabled: true
19
+ EnforcedStyleForMultiline: consistent_comma
20
+
21
+ Lint/NumberConversion:
22
+ Enabled: true
23
+
24
+ RSpec/ExampleLength:
25
+ Max: 12
26
+
27
+ RSpec/MultipleExpectations:
28
+ Max: 3
data/CHANGELOG.md ADDED
@@ -0,0 +1,113 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+
11
+ -
12
+
13
+ ### Changed
14
+
15
+ -
16
+
17
+ ## [1.0.0]
18
+
19
+ ### Added
20
+
21
+ - `Logplex::HTTP::Error` exception hierarchy. See: lib/logplex/errors.rb
22
+ - RuboCop linting with StandardRB-based configuration
23
+ - GitHub Actions CI workflow (Ruby 3.2, 3.3, 3.4, 4.0)
24
+
25
+ ### Changed
26
+
27
+ - Migrated test stubs from Excon mocks to WebMock
28
+ - Migrated CI from CircleCI to GitHub Actions
29
+
30
+ ### Removed
31
+
32
+ - **Breaking Change:** Replace `Excon` with stdlib `Net::HTTP` - Gem-specific errors are raised rather than Excon-specific errors. See: lib/logplex/errors.rb
33
+ - Duplicate license file
34
+
35
+ ## [0.0.7]
36
+
37
+ ### Added
38
+
39
+ - Bearer authentication support via `bearer_token:` keyword argument on `Publisher`
40
+
41
+ ### Changed
42
+
43
+ - N/A
44
+
45
+ ### Removed
46
+
47
+ - CircleCI configuration
48
+ - Duplicate license file
49
+
50
+ ## [0.0.6] - 2022-03-03
51
+
52
+ ### Added
53
+
54
+ - Accept HTTP 202 responses from Logplex in addition to 200 and 204
55
+ - CODEOWNERS file with ECCN classification
56
+
57
+ ### Changed
58
+
59
+ - Migrated CI from Travis CI to CircleCI
60
+
61
+ ### Removed
62
+
63
+ - Travis CI configuration
64
+
65
+ ## [0.0.5] - 2021-09-13
66
+
67
+ ### Changed
68
+
69
+ - Updated gemspec metadata
70
+
71
+ ## [0.0.4] - 2021-07-13
72
+
73
+ ### Added
74
+
75
+ - `Logplex-Msg-Count` header sent with each publish request
76
+ - `app_name` option to override the default token-based app name on publish
77
+ - Tests for header correctness and app name override
78
+
79
+ ### Fixed
80
+
81
+ - Message number mismatch when publishing multiple messages
82
+ - `app_name` from options now properly overrides the token default instead of the reverse
83
+
84
+ ## [0.0.1] - 2016-07-26
85
+
86
+ ### Changed
87
+
88
+ - Modernized publisher to use `Excon.post` directly instead of persistent connections
89
+ - Updated gemspec and dependencies
90
+
91
+ ## [0.0.1-pre] - 2013-05-01
92
+
93
+ ### Added
94
+
95
+ - Initial logplex publisher and message formatting
96
+ - Global configuration object with configurable `process`, `host`, and `publish_timeout`
97
+ - Syslog-framed message encoding
98
+ - Key/value pair formatting when messages are passed as a hash
99
+ - Publish timeout (default 1 second) with configurable override
100
+ - Return value from `publish` (true on success, false on failure)
101
+ - Travis CI configuration
102
+
103
+ ### Fixed
104
+
105
+ - Bug caused by invoking `Array()` on a hash
106
+
107
+ [Unreleased]: https://github.com/heroku/logplex-gem/compare/v1.0.0...HEAD
108
+ [1.0.0]: https://github.com/heroku/logplex-gem/compare/v0.0.7...v1.0.0
109
+ [0.0.7]: https://github.com/heroku/logplex-gem/compare/v0.0.6...0.0.7
110
+ [0.0.6]: https://github.com/heroku/logplex-gem/compare/v0.0.5...v0.0.6
111
+ [0.0.5]: https://github.com/heroku/logplex-gem/compare/v0.0.4...v0.0.5
112
+ [0.0.4]: https://github.com/heroku/logplex-gem/compare/v0.0.1...v0.0.4
113
+ [0.0.1]: https://github.com/heroku/logplex-gem/compare/v0.0.1-pre...v0.0.1
data/CODEOWNERS CHANGED
@@ -1,2 +1,4 @@
1
1
  # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing.
2
2
  #ECCN:Open Source
3
+ #GUSINFO:Heroku Data Operations,Heroku Data Intake
4
+ * @heroku/data
data/Gemfile CHANGED
@@ -1,4 +1,11 @@
1
- source 'https://rubygems.org'
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
2
4
 
3
- # Specify your gem's dependencies in logplex.gemspec
4
5
  gemspec
6
+
7
+ gem "rspec"
8
+ gem "rubocop"
9
+ gem "rubocop-performance"
10
+ gem "rubocop-rspec"
11
+ gem "webmock"
data/README.md CHANGED
@@ -2,10 +2,9 @@
2
2
 
3
3
  Publish and Consume Logplex messages
4
4
 
5
- Logplex is the Heroku log router, and can be found
6
- [here](https://github.com/heroku/logplex).
5
+ Logplex is the Heroku log router, and [can be found here](https://github.com/heroku/logplex).
7
6
 
8
- ### Publishing messages
7
+ ## Publishing messages
9
8
 
10
9
  ```ruby
11
10
  publisher = Logplex::Publisher.new(logplex_url)
@@ -57,7 +56,34 @@ publisher = Logplex::Publisher.new
57
56
  publisher.publish "And she's buying a stairway to heaven"
58
57
  ```
59
58
 
60
- ### License
59
+ ## Releasing
60
+
61
+ 1. **Bump the version** in `lib/logplex/version.rb`
62
+
63
+ 2. **Update `CHANGELOG.md`** — move entries from `[Unreleased]` into a new versioned section and update the comparison links at the bottom
64
+
65
+ 3. **Commit the changes**
66
+
67
+ ```shell
68
+ git add lib/logplex/version.rb CHANGELOG.md
69
+ git commit -m "version -> x.y.z"
70
+ ```
71
+
72
+ 4. **Create and push a git tag**
73
+
74
+ ```shell
75
+ git tag vx.y.z
76
+ git push origin main --tags
77
+ ```
78
+
79
+ 5. **Build and push the gem to RubyGems.org**
80
+
81
+ ```shell
82
+ gem build
83
+ gem push logplex-x.y.z.gem
84
+ ```
85
+
86
+ ## License
61
87
 
62
88
  Copyright (c) Harold Giménez. Released under the terms of the MIT License found
63
89
  in the LICENSE file.
data/Rakefile CHANGED
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
@@ -1,16 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Logplex
2
4
  class Configuration
3
5
  attr_accessor :logplex_url,
4
- :app_name,
5
- :process,
6
- :host,
7
- :publish_timeout
6
+ :app_name,
7
+ :process,
8
+ :host,
9
+ :publish_timeout
8
10
 
9
11
  def initialize
10
- @logplex_url = 'https://east.logplex.io/logs'
11
- @host = 'localhost'
12
+ @logplex_url = "https://east.logplex.io/logs"
13
+ @host = "localhost"
12
14
  @publish_timeout = 1
13
- @app_name = 'app'
15
+ @app_name = "app"
14
16
  end
15
17
  end
16
18
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logplex
4
+ module HTTP
5
+ class Error < StandardError; end
6
+ class ConnectionResetError < Error; end
7
+ class SocketError < Error; end
8
+ class TimeoutError < Error; end
9
+ class ServerError < Error; end
10
+ class ServiceUnavailableError < ServerError; end
11
+ class ClientError < Error; end
12
+ class SeeOtherError < Error; end
13
+ end
14
+ end
@@ -1,6 +1,8 @@
1
- require 'valcro'
2
- require 'time'
3
- require 'logplex/configuration'
1
+ # frozen_string_literal: true
2
+
3
+ require "valcro"
4
+ require "time"
5
+ require "logplex/configuration"
4
6
 
5
7
  module Logplex
6
8
  class Message
@@ -8,16 +10,16 @@ module Logplex
8
10
 
9
11
  # facility = local0, priority = info, RFC5452 encoded
10
12
  # syslog version 1
11
- FACILITY_AND_PRIORITY = '<134>1'.freeze
13
+ FACILITY_AND_PRIORITY = "<134>1"
12
14
 
13
- FIELD_DISABLED = '-'.freeze
15
+ FIELD_DISABLED = "-"
14
16
 
15
17
  def initialize(message, opts = {})
16
- @message = message
17
- @app_name = opts[:app_name] || Logplex.configuration.app_name
18
- @time = opts[:time] || DateTime.now
19
- @process = opts[:process] || Logplex.configuration.process
20
- @host = opts[:host] || Logplex.configuration.host
18
+ @message = message
19
+ @app_name = opts[:app_name] || Logplex.configuration.app_name
20
+ @time = opts[:time] || DateTime.now
21
+ @process = opts[:process] || Logplex.configuration.process
22
+ @host = opts[:host] || Logplex.configuration.host
21
23
  @message_id = opts[:message_id] || FIELD_DISABLED
22
24
  end
23
25
 
@@ -48,8 +50,8 @@ module Logplex
48
50
  def formatted_message
49
51
  if @message.is_a?(Hash)
50
52
  @message.inject([]) do |res, (key, value)|
51
- res << %{#{key}="#{value}"}
52
- end.join(' ')
53
+ res << %(#{key}="#{value}")
54
+ end.join(" ")
53
55
  else
54
56
  @message
55
57
  end
@@ -1,20 +1,20 @@
1
- # encoding: UTF-8
2
- require 'excon'
3
- require 'logplex/message'
4
- require 'timeout'
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "logplex/errors"
6
+ require "logplex/message"
7
+ require "timeout"
5
8
 
6
9
  module Logplex
7
10
  class Publisher
8
- PUBLISH_ERRORS = [Excon::Errors::InternalServerError,
9
- Excon::Errors::Unauthorized,
10
- Timeout::Error].freeze
11
-
12
- def initialize(logplex_url = nil)
11
+ def initialize(logplex_url = nil, bearer_token: nil)
13
12
  @logplex_url = logplex_url || Logplex.configuration.logplex_url
14
13
  @token = URI(@logplex_url).password || Logplex.configuration.app_name
14
+ @auth_headers = bearer_token ? { "Authorization" => "Bearer #{bearer_token}" } : {}
15
15
  end
16
16
 
17
- def publish(messages, opts={})
17
+ def publish(messages, opts = {})
18
18
  message_list = messages.dup
19
19
  unless messages.is_a? Array
20
20
  message_list = [message_list]
@@ -22,25 +22,55 @@ module Logplex
22
22
  message_list.map! { |m| Message.new(m, { app_name: @token }.merge(opts)) }
23
23
  message_list.each(&:validate)
24
24
  if message_list.inject(true) { |accum, m| m.valid? }
25
- begin
26
- Timeout.timeout(Logplex.configuration.publish_timeout) do
27
- api_post(message_list.map(&:syslog_frame).join(''), message_list.length)
28
- true
29
- end
30
- rescue *PUBLISH_ERRORS
31
- false
25
+ Timeout.timeout(Logplex.configuration.publish_timeout) do
26
+ api_post(message_list.map(&:syslog_frame).join(""), message_list.length)
27
+ true
32
28
  end
33
29
  end
30
+ rescue Errno::ECONNRESET => e
31
+ raise Logplex::HTTP::ConnectionResetError, e.message
32
+ rescue ::SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH => e
33
+ raise Logplex::HTTP::SocketError, e.message
34
+ rescue Timeout::Error => e
35
+ raise Logplex::HTTP::TimeoutError, e.message
34
36
  end
35
37
 
36
38
  private
37
39
 
38
40
  def api_post(message, number_messages)
39
- Excon.post(@logplex_url, body: message, headers: {
40
- "Content-Type" => 'application/logplex-1',
41
- "Content-Length" => message.length,
42
- "Logplex-Msg-Count" => number_messages
43
- }, expects: [200, 202, 204])
41
+ uri = URI(@logplex_url)
42
+ http = Net::HTTP.new(uri.host, uri.port)
43
+ http.use_ssl = uri.scheme == "https"
44
+
45
+ request = Net::HTTP::Post.new(uri)
46
+ request.body = message
47
+ request["Content-Type"] = "application/logplex-1"
48
+ request["Content-Length"] = message.length
49
+ request["Logplex-Msg-Count"] = number_messages
50
+
51
+ if @auth_headers.key?("Authorization")
52
+ request["Authorization"] = @auth_headers["Authorization"]
53
+ elsif uri.password
54
+ request.basic_auth(uri.user, uri.password)
55
+ end
56
+
57
+ response = http.request(request)
58
+
59
+ code = Integer(response.code, 10)
60
+ return if [200, 202, 204].include?(code)
61
+
62
+ case code
63
+ when 303
64
+ raise Logplex::HTTP::SeeOtherError, "Unexpected response: #{response.code}"
65
+ when 503
66
+ raise Logplex::HTTP::ServiceUnavailableError, "Unexpected response: #{response.code}"
67
+ when 400..499
68
+ raise Logplex::HTTP::ClientError, "Unexpected response: #{response.code}"
69
+ when 500..599
70
+ raise Logplex::HTTP::ServerError, "Unexpected response: #{response.code}"
71
+ else
72
+ raise Logplex::HTTP::Error, "Unexpected response: #{response.code}"
73
+ end
44
74
  end
45
75
  end
46
76
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Logplex
2
- VERSION = "0.0.6".freeze
4
+ VERSION = "1.0.0"
3
5
  end
data/lib/logplex.rb CHANGED
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "logplex/version"
2
4
  require "logplex/configuration"
5
+ require "logplex/errors"
3
6
  require "logplex/message"
4
7
  require "logplex/publisher"
data/logplex.gemspec CHANGED
@@ -1,22 +1,25 @@
1
- # -*- encoding: utf-8 -*-
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'logplex/version'
1
+ # frozen_string_literal: true
5
2
 
6
- Gem::Specification.new do |gem|
7
- gem.name = "logplex"
8
- gem.version = Logplex::VERSION
9
- gem.authors = ["Harold Giménez", "Heroku"]
10
- gem.email = ["harold.gimenez@gmail.com"]
11
- gem.description = "Publish and Consume Logplex messages"
12
- gem.summary = "Publish and Consume Logplex messages"
13
- gem.homepage = "https://github.com/heroku/logplex-gem"
3
+ require_relative "lib/logplex/version"
14
4
 
15
- gem.files = `git ls-files`.split($/)
16
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
- gem.require_paths = ["lib"]
19
- gem.add_dependency "valcro"
20
- gem.add_dependency "excon"
21
- gem.add_development_dependency "rspec"
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "logplex"
7
+ spec.version = Logplex::VERSION
8
+ spec.authors = ["Harold Giménez", "Heroku"]
9
+ spec.email = ["harold.gimenez@gmail.com"]
10
+ spec.description = "Logplex client"
11
+ spec.summary = "Publish and Consume Logplex messages"
12
+ spec.homepage = "https://github.com/heroku/logplex-gem"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 3.2.0"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/heroku/logplex-gem"
18
+ spec.metadata["changelog_uri"] = "https://github.com/heroku/logplex-gem/blob/main/CHANGELOG.md"
19
+
20
+ spec.files = `git ls-files`.split($/)
21
+ spec.executables = spec.files.grep(%r{^bin/}).map { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "valcro"
22
25
  end
@@ -1,14 +1,16 @@
1
- require 'spec_helper'
2
- require 'logplex/configuration'
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "logplex/configuration"
3
5
 
4
6
  describe Logplex::Configuration do
5
- describe 'defaults' do
6
- it 'defaults to the production heroku logplex url' do
7
+ describe "defaults" do
8
+ it "defaults to the production heroku logplex url" do
7
9
  Logplex.configure { |config| }
8
10
 
9
11
  expect(
10
- Logplex.configuration.logplex_url
11
- ).to eq('https://east.logplex.io/logs')
12
+ Logplex.configuration.logplex_url,
13
+ ).to eq("https://east.logplex.io/logs")
12
14
  end
13
15
  end
14
16
  end
@@ -1,63 +1,66 @@
1
- require 'spec_helper'
2
- require 'logplex/message'
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "logplex/message"
3
5
 
4
6
  describe Logplex::Message do
5
7
  before { Logplex.configure {} }
6
8
  after { restore_default_config }
7
- it 'fills out fields of a syslog message' do
8
- message = Logplex::Message.new(
9
- 'my message here',
10
- app_name: 't.some-token',
9
+
10
+ it "fills out fields of a syslog message" do
11
+ message = described_class.new(
12
+ "my message here",
13
+ app_name: "t.some-token",
11
14
  time: DateTime.parse("1980-08-23 05:31 00:00"),
12
- process: 'heroku-postgres',
13
- host: 'some-host',
14
- message_id: '1'
15
+ process: "heroku-postgres",
16
+ host: "some-host",
17
+ message_id: "1",
15
18
  )
16
19
 
17
20
  expect(message.syslog_frame).to eq(
18
- "91 <134>1 1980-08-23T05:31:00+00:00 some-host t.some-token heroku-postgres 1 - my message here"
21
+ "91 <134>1 1980-08-23T05:31:00+00:00 some-host t.some-token heroku-postgres 1 - my message here",
19
22
  )
20
23
  end
21
24
 
22
- it 'is invalid for messages longer than 10240 bytes' do
23
- short = Logplex::Message.new('a' * 10240, app_name: 'foo',
24
- process: 'proc',
25
- host: 'host')
26
- long = Logplex::Message.new('a' * 10241, app_name: 'foo',
27
- process: 'proc',
28
- host: 'host')
25
+ it "is invalid for messages longer than 10240 bytes" do
26
+ short = described_class.new("a" * 10240, app_name: "foo",
27
+ process: "proc",
28
+ host: "host",)
29
+ long = described_class.new("a" * 10241, app_name: "foo",
30
+ process: "proc",
31
+ host: "host",)
29
32
  short.validate
30
33
  long.validate
31
34
 
32
- expect(short.valid?).to be_truthy
33
- expect(long.valid?).to be_falsey
35
+ expect(short).to be_valid
36
+ expect(long).not_to be_valid
34
37
  end
35
38
 
36
- it 'is invalid with no process or host' do
39
+ it "is invalid with no process or host" do
37
40
  Logplex.configure do |conf|
38
- conf.host = nil
41
+ conf.host = nil
39
42
  conf.process = nil
40
43
  end
41
44
 
42
- message = Logplex::Message.new("a message", app_name: 't.some-token')
45
+ message = described_class.new("a message", app_name: "t.some-token")
43
46
  message.validate
44
47
 
45
- expect(message.valid?).to be_falsey
48
+ expect(message).not_to be_valid
46
49
  expect(message.errors[:process]).to eq ["can't be nil"]
47
50
  expect(message.errors[:host]).to eq ["can't be nil"]
48
51
  end
49
52
 
50
- it 'formats logs as key/values when given a hash' do
51
- message = Logplex::Message.new(
52
- { vocals: 'Robert Plant', guitar: 'Jimmy Page' },
53
- app_name: 't.some-token',
54
- process: 'proc',
55
- host: 'host',
56
- time: DateTime.parse("1980-08-23 05:31 00:00")
53
+ it "formats logs as key/values when given a hash" do
54
+ message = described_class.new(
55
+ { vocals: "Robert Plant", guitar: "Jimmy Page" },
56
+ app_name: "t.some-token",
57
+ process: "proc",
58
+ host: "host",
59
+ time: DateTime.parse("1980-08-23 05:31 00:00"),
57
60
  )
58
61
 
59
62
  expect(message.syslog_frame).to eq(
60
- %{101 <134>1 1980-08-23T05:31:00+00:00 host t.some-token proc - - vocals="Robert Plant" guitar="Jimmy Page"}
63
+ %(101 <134>1 1980-08-23T05:31:00+00:00 host t.some-token proc - - vocals="Robert Plant" guitar="Jimmy Page"),
61
64
  )
62
65
  end
63
66
  end