diff --git a/.rubocop.yml b/.rubocop.yml index bd8c31c..deaad11 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,7 +1,3 @@ -# Commonly used screens these days easily fit more than 80 characters. -Metrics/LineLength: - Max: 120 - # Too short methods lead to extraction of single-use methods, which can make # the code easier to read (by naming things), but can also clutter the class Metrics/MethodLength: @@ -20,9 +16,12 @@ Metrics/CyclomaticComplexity: Metrics/PerceivedComplexity: Max: 40 +# Commonly used screens these days easily fit more than 80 characters. +Layout/LineLength: + Max: 120 + # No space makes the method definition shorter and differentiates # from a regular assignment. - Layout/AccessModifierIndentation: Enabled: true IndentationWidth: 4 @@ -56,9 +55,6 @@ Style/CollectionMethods: # inject seems more common in the community. reduce: "inject" -Style/RedundantInterpolation: - Enabled: false - Style/RescueStandardError: Enabled: false @@ -66,8 +62,6 @@ Style/RescueStandardError: # is silly. Let's try to live without them for now. Style/ParenthesesAroundCondition: AllowSafeAssignment: false -Lint/AssignmentInCondition: - AllowSafeAssignment: false # A specialized exception class will take one or more arguments and construct the message from it. # So both variants make sense. @@ -80,10 +74,7 @@ Style/RaiseArgs: Style/SignalException: EnforcedStyle: only_raise -# Suppressing exceptions can be perfectly fine, and be it to avoid to -# explicitly type nil into the rescue since that's what you want to return, -# or suppressing LoadError for optional dependencies -Lint/SuppressedException: +Style/GuardClause: Enabled: false # { ... } for multi-line blocks is okay, follow Weirichs rule instead: @@ -110,6 +101,9 @@ Style/SingleLineBlockParams: Lint/ShadowingOuterLocalVariable: Enabled: false +Lint/AssignmentInCondition: + Enabled: false + # Check with yard instead. Style/Documentation: Enabled: false @@ -123,9 +117,26 @@ Naming/BinaryOperatorParameterName: Lint/Debugger: Enabled: false +Security/Eval: + Enabled: false # Style preference Style/MethodDefParentheses: Enabled: false Style/TrailingCommaInHashLiteral: Enabled: false + +Style/IfUnlessModifier: + Enabled: false + +Lint/DuplicateMethods: + Enabled: false + +Style/RedundantSelf: + Enabled: false + +Style/NegatedIf: + Enabled: false + +Style/SafeNavigation: + Enabled: false diff --git a/Gemfile b/Gemfile index bb242f2..5ff7bb8 100755 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,7 @@ -source 'https://rubygems.org' +# frozen_string_literal: true + +source "https://rubygems.org" # Specify your gem's dependencies in logdna_ruby.gemspec gemspec +gem "minitest" diff --git a/lib/logdna.rb b/lib/logdna.rb index a3351cd..4781277 100755 --- a/lib/logdna.rb +++ b/lib/logdna.rb @@ -1,12 +1,12 @@ -#!/usr/bin/env ruby # frozen_string_literal: true -# require 'singleton' +require "logger" require "socket" require "uri" require_relative "logdna/client.rb" require_relative "logdna/resources.rb" require_relative "logdna/version.rb" + module Logdna class ValidURLRequired < ArgumentError; end class MaxLengthExceeded < ArgumentError; end @@ -22,11 +22,13 @@ def initialize(key, opts = {}) @level = opts[:level] || "INFO" @env = opts[:env] @meta = opts[:meta] + @internal_logger = Logger.new(STDOUT) + @internal_logger.level = Logger::DEBUG endpoint = opts[:endpoint] || Resources::ENDPOINT hostname = opts[:hostname] || Socket.gethostname if hostname.size > Resources::MAX_INPUT_LENGTH || @app.size > Resources::MAX_INPUT_LENGTH - puts "Hostname or Appname is over #{Resources::MAX_INPUT_LENGTH} characters" + @internal_logger.debug("Hostname or Appname is over #{Resources::MAX_INPUT_LENGTH} characters") return end @@ -64,7 +66,7 @@ def log(message = nil, opts = {}) message = yield end if message.nil? - puts "provide either a message or block" + @internal_logger.debug("provide either a message or block") return end message = message.to_s.encode("UTF-8") @@ -103,7 +105,7 @@ def <<(msg = nil, opts = {}) end def add(*_arg) - puts "add not supported in LogDNA logger" + @internal_logger.debug("add not supported in LogDNA logger") false end @@ -114,20 +116,16 @@ def unknown(msg = nil, opts = {}) end def datetime_format(*_arg) - puts "datetime_format not supported in LogDNA logger" + @internal_logger.debug("datetime_format not supported in LogDNA logger") false end def close - if !@client.nil? - @client.exitout - end + @client&.exitout end at_exit do - if !@client.nil? - @client.exitout - end + @client&.exitout end end end diff --git a/lib/logdna/client.rb b/lib/logdna/client.rb index b26434e..29290af 100755 --- a/lib/logdna/client.rb +++ b/lib/logdna/client.rb @@ -26,6 +26,9 @@ def initialize(request, uri, opts) @request = request @retry_timeout = opts[:retry_timeout] || Resources::RETRY_TIMEOUT + + @internal_logger = Logger.new(STDOUT) + @internal_logger.level = Logger::DEBUG end def process_message(msg, opts = {}) @@ -81,7 +84,7 @@ def send_request }.to_json handle_exception = lambda do |message| - puts message + @internal_logger.debug(message) @exception_flag = true @side_message_lock.synchronize do @side_messages.concat(@buffer) @@ -97,9 +100,9 @@ def send_request http.request(@request) end if @response.is_a?(Net::HTTPForbidden) - puts "Please provide a valid ingestion key" + @internal_logger.debug("Please provide a valid ingestion key") elsif !@response.is_a?(Net::HTTPSuccess) - handle_exception.call("The response is not successful ") + handle_exception.call("The response is not successful #{@response}") end @exception_flag = false rescue SocketError @@ -114,7 +117,6 @@ def send_request end def flush - if @lock.try_lock @flush_scheduled = false if @buffer.any? || @side_messages.any? @@ -128,7 +130,7 @@ def flush def exitout flush - puts "Exiting LogDNA logger: Logging remaining messages" + @internal_logger.debug("Exiting LogDNA logger: Logging remaining messages") end end end diff --git a/lib/logdna/resources.rb b/lib/logdna/resources.rb index b1323c4..eceb23b 100755 --- a/lib/logdna/resources.rb +++ b/lib/logdna/resources.rb @@ -1,15 +1,17 @@ +# frozen_string_literal: true + module Resources - LOG_LEVELS = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL', 'TRACE'].freeze - DEFAULT_REQUEST_HEADER = { 'Content-Type' => 'application/json; charset=UTF-8' }.freeze - DEFAULT_REQUEST_TIMEOUT = 180000 - MS_IN_A_DAY = 86400000 - MAX_REQUEST_TIMEOUT = 300000 - MAX_LINE_LENGTH = 32000 + LOG_LEVELS = %w[DEBUG INFO WARN ERROR FATAL TRACE] + DEFAULT_REQUEST_HEADER = { "Content-Type" => "application/json; charset=UTF-8" } + DEFAULT_REQUEST_TIMEOUT = 180_000 + MS_IN_A_DAY = 86_400_000 + MAX_REQUEST_TIMEOUT = 300_000 + MAX_LINE_LENGTH = 32_000 MAX_INPUT_LENGTH = 80 RETRY_TIMEOUT = 60 FLUSH_INTERVAL = 0.25 - FLUSH_BYTE_LIMIT = 500000 - ENDPOINT = 'https://logs.logdna.com/logs/ingest'.freeze + FLUSH_BYTE_LIMIT = 500_000 + ENDPOINT = "https://logs.logdna.com/logs/ingest" MAC_ADDR_CHECK = /^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$/ IP_ADDR_CHECK = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ end diff --git a/lib/logdna/version.rb b/lib/logdna/version.rb old mode 100755 new mode 100644 index 944e6b7..363dd5c --- a/lib/logdna/version.rb +++ b/lib/logdna/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module LogDNA - VERSION = '1.3.0'.freeze + VERSION = "1.3.0" end diff --git a/logdna.gemspec b/logdna.gemspec index be05c27..b0cdea2 100755 --- a/logdna.gemspec +++ b/logdna.gemspec @@ -1,22 +1,23 @@ -# coding: utf-8 -lib = File.expand_path('../lib', __FILE__) +# frozen_string_literal: true + +lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'logdna/version' +version = File.open("lib/logdna/version.rb").read.scan(/"([^"]*)"/).first.first Gem::Specification.new do |spec| - spec.name = 'logdna' - spec.version = LogDNA::VERSION - spec.authors = 'Gun Woo Choi, Derek Zhou, Vilya Levitskiy, Muaz Siddiqui' - spec.email = 'support@logdna.com' - spec.summary = 'LogDNA Ruby logger' - spec.homepage = 'https://github.com/logdna/ruby' - spec.license = 'MIT' - spec.files = Dir.glob("{lib}/**/*.rb") + %w(LICENSE README.md) - spec.bindir = 'exe' + spec.name = "logdna" + spec.version = version + spec.authors = "Gun Woo Choi, Derek Zhou, Vilya Levitskiy, Muaz Siddiqui" + spec.email = "support@logdna.com" + spec.summary = "LogDNA Ruby logger" + spec.homepage = "https://github.com/logdna/ruby" + spec.license = "MIT" + spec.files = Dir.glob("{lib}/**/*.rb") + %w[LICENSE README.md] + spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } - spec.require_paths = ['lib'] - spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0' - spec.add_runtime_dependency 'require_all', '~> 1.4' - spec.add_runtime_dependency 'json', '~> 2.0' - spec.add_development_dependency 'rubocop', '~> 0.78' + spec.require_paths = ["lib"] + spec.add_runtime_dependency "concurrent-ruby", "~> 1.0" + spec.add_runtime_dependency "json", "~> 2.0" + spec.add_runtime_dependency "require_all", "~> 1.4" + spec.add_development_dependency "rubocop", "~> 0.78" end diff --git a/test_server.rb b/test_server.rb new file mode 100644 index 0000000..101e96a --- /dev/null +++ b/test_server.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "socket" + +class TestServer + def start_server(port) + server = TCPServer.new(port) + puts server + data = "" + + Thread.start(server.accept) { |client| + headers = {} + while line = client.gets.split(" ", 2) + break if line[0] == "" + + headers[line[0].chop] = line[1].strip + end + data = client.read(headers["Content-Length"].to_i) + client.puts("HTTP/1.1 200 OK") + client.close + }.join + + eval(data) + end + + def accept_logs_and_respond(server, data, res) + Thread.start(server.accept) { |client| + headers = {} + while line = client.gets.split(" ", 2) + break if line[0] == "" + + headers[line[0].chop] = line[1].strip + end + data += client.read(headers["Content-Length"].to_i) + client.puts(res) + client.close + }.join + + data + end + + def return_not_found_res(port) + server = TCPServer.new(port) + data = "" + accept_logs_and_respond(server, data, "HTTP/1.1 404 Not Found") + data += accept_logs_and_respond(server, data, "HTTP/1.1 200 OK") + + eval(data) + end +end diff --git a/tests.rb b/tests.rb new file mode 100644 index 0000000..5eba214 --- /dev/null +++ b/tests.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require "minitest/autorun" + +require_relative "lib/logdna.rb" +require_relative "lib/logdna/client.rb" +require_relative "test_server.rb" + +class TestLogDNARuby < Minitest::Test + LOG_LINE = "log line" + + def get_options(port) + { + hostname: "rubyTestHost", + ip: "75.10.4.81", + mac: "00:00:00:a1:2b:cc", + app: "rubyApplication", + level: "INFO", + env: "PRODUCTION", + endpoint: "http://localhost:#{port}", + flush_interval: 1, + flush_size: 5, + retry_timeout: 1 + } + end + + def log_level_test(level, port, expected_level) + options = get_options(port) + logdna_thread = Thread.start do + logger = Logdna::Ruby.new("pp", options) + logger.send(level, LOG_LINE) + end + + server_thread = Thread.start do + server = TestServer.new + recieved_data = server.start_server(port) + assert_equal(recieved_data[:ls][0][:line], LOG_LINE) + assert_equal(recieved_data[:ls][0][:app], options[:app]) + assert_equal(recieved_data[:ls][0][:level], expected_level) + assert_equal(recieved_data[:ls][0][:env], options[:env]) + end + + logdna_thread.join + server_thread.join + end + + # Should retry to connect and preserve the failed line + def fatal_method_not_found(level, port, expected_level) + second_line = " second line" + options = get_options(port) + logdna_thread = Thread.start do + logger = Logdna::Ruby.new("pp", options) + logger.send(level, LOG_LINE) + logger.send(level, second_line) + end + + server_thread = Thread.start do + sor = TestServer.new + recieved_data = sor.return_not_found_res(port) + # The order of recieved lines is unpredictable. + recieved_lines = [ + recieved_data[:ls][0][:line], + recieved_data[:ls][1][:line] + ] + + assert_includes(recieved_lines, LOG_LINE) + assert_includes(recieved_lines, second_line) + + assert_equal(recieved_data[:ls][0][:app], options[:app]) + assert_equal(recieved_data[:ls][0][:level], expected_level) + assert_equal(recieved_data[:ls][0][:env], options[:env]) + end + + logdna_thread.join + server_thread.join + end + + def test_all + log_level_test("warn", 2000, "WARN") + log_level_test("info", 2001, "INFO") + log_level_test("fatal", 2002, "FATAL") + log_level_test("debug", 2003, "DEBUG") + fatal_method_not_found("fatal", 2004, "FATAL") + end +end