diff --git a/README.md b/README.md index 158de1b..4a428fd 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,41 @@ A simple UDP client for [InfluxDB](https://influxdb.com) :construction: :warning: **This project is in very stage. Things will change!** +## Install + + $ [sudo] gem install linr + +Or add it to your Gemfile, etc. + +## Usage + +Connect to a InfluxDB host via +[UDP](https://influxdb.com/docs/v0.9/write_protocols/udp.html) +and send a series: + +```ruby +require "linr" + +client = Linr::Client.new(host: "127.0.0.1", port: 8836) +client.write( + measurement: "cpu_load_short", + tags: { host: "server01", region: "us-west" }, + fields: { value: 0.64 }, + timestamp: 1434055562 +) +``` + ## References * [Line Protocol Syntax](https://influxdb.com/docs/v0.9/write_protocols/write_syntax.html) * [InfluxDB UDP](https://influxdb.com/docs/v0.9/write_protocols/udp.html) +## Testing + +Just run + + $ rake + ## Contributing diff --git a/lib/linr.rb b/lib/linr.rb index 01a0ad8..df3397f 100644 --- a/lib/linr.rb +++ b/lib/linr.rb @@ -1,7 +1,10 @@ require "linr/version" +require "linr/config" +require "linr/client" require "linr/data" require "linr/encoder" require "linr/connection" +require "linr/logger" # A simple UDP client for InfluxDB module Linr diff --git a/lib/linr/client.rb b/lib/linr/client.rb new file mode 100644 index 0000000..ce86d67 --- /dev/null +++ b/lib/linr/client.rb @@ -0,0 +1,60 @@ +module Linr + # Simple UDP-based InfluxDB client + # @example + # client = Client.new(host: "127.0.0.1", port: 8063) + # client.write( + # measurement: "cpu_load_short", + # tags: { host: "server01", region: "us-west" }, + # fields: { value: 0.64 }, + # timestamp: 1434055562 + # ) + class Client + attr_reader :config + + # Initialize a new client. + # @param opts [Hash] See {Config} for opts + # @see Config + def initialize(opts = {}) + @config = Config.new(opts) + connect! + end + + # Write one or more series to the InfluxDB connection + # @param series [Array] see {Data} for values + def write(*series) + payload = build_payload(series) + send(payload) + end + + private + + def connect! + @connection = Connection::UDP.new(config.host, config.port) + info("Connected to #{config.host}:#{config.port}") + end + + def build_payload(series) + series.map do |serie| + data = Data.new(serie) + encode(data) + end.join("\n") + end + + def send(payload) + debug(payload) + @connection.send(payload) + end + + def encode(data) + config.encoder.dump(data) + end + + def info(matter) + config.logger.info("Linr") { matter } + end + + def debug(matter) + config.logger.debug("Linr") { matter } + end + end +end diff --git a/lib/linr/config.rb b/lib/linr/config.rb new file mode 100644 index 0000000..bf7bc3f --- /dev/null +++ b/lib/linr/config.rb @@ -0,0 +1,24 @@ +module Linr + # Holds the config of a {Client} + # @example + # Config.new(host: "db.domain.com", logger: Logger.new($stdout)) + class Config + attr_reader :host + attr_reader :port + attr_reader :logger + attr_reader :encoder + + # Build a config based on opts + # @param opts [Hash] the options to create the config + # @option opts [String] :host + # @option opts [Fixnum] :port + # @option opts [::Logger] :logger + # @option opts [Object] :encoder + def initialize(opts = {}) + @host = opts.fetch(:host, "127.0.0.1") + @port = opts.fetch(:port, 8089) + @logger = opts.fetch(:logger, Logger::Null.new) + @encoder = opts.fetch(:encoder, Encoder::Line.new) + end + end +end diff --git a/lib/linr/data.rb b/lib/linr/data.rb index c281486..2f68baa 100644 --- a/lib/linr/data.rb +++ b/lib/linr/data.rb @@ -1,4 +1,13 @@ module Linr + # Describes series send to InfluxDB + # @see https://influxdb.com/docs/v0.9/guides/writing_data.html + # @example + # Data.new( + # measurement: cpu_load_short, + # tags: { host: "server01", region: "us-west" }, + # fields: { value: 0.64 }, + # timestamp: 1434055562 + # ) class Data attr_reader :measurement attr_reader :fields diff --git a/lib/linr/logger.rb b/lib/linr/logger.rb new file mode 100644 index 0000000..6225329 --- /dev/null +++ b/lib/linr/logger.rb @@ -0,0 +1 @@ +require "linr/logger/null" diff --git a/lib/linr/logger/null.rb b/lib/linr/logger/null.rb new file mode 100644 index 0000000..a4c41f0 --- /dev/null +++ b/lib/linr/logger/null.rb @@ -0,0 +1,13 @@ +module Linr + module Logger + # A simple null logger without any output + # @api private + class Null < ::Logger + def initialize(*args) + end + + def add(*args, &block) + end + end + end +end diff --git a/spec/client_spec.rb b/spec/client_spec.rb new file mode 100644 index 0000000..b262d2e --- /dev/null +++ b/spec/client_spec.rb @@ -0,0 +1,47 @@ +require "spec_helper" + +describe ::Linr::Client do + subject { ::Linr::Client.new(config) } + let(:config) do + { port: port, logger: logger } + end + let(:logger) do + ::Logger.new(output).tap { |l| l.level = Logger::DEBUG } + end + let(:output) { StringIO.new } + let(:socket) { UDPSocket.new } + let(:port) { 44_001 } + + before do + socket.bind("127.0.0.1", port) + end + + after do + socket.close + end + + it "holds a config" do + subject.config.logger.must_equal logger + end + + it "writes a single series to socket" do + subject.write(measurement: "m", fields: { a: 1.0 }) + socket.recvfrom(60).first.must_equal "m a=1.0" + end + + it "writes multiple series to socket" do + serie1 = { measurement: "m", fields: { a: 1.0 } } + serie2 = { measurement: "n", fields: { b: true }, timestamp: 1_234_567 } + subject.write(serie1, serie2) + + socket.recvfrom(80).first.must_equal "m a=1.0\nn b=true 1234567" + end + + it "logs payloads" do + subject.write(measurement: "m", fields: { a: 1.0 }) + lines = output.string.split("\n") + lines.size.must_equal 2 + lines.shift.must_match(/Connected to 127\.0\.0\.1:44001/) + lines.shift.must_match("m a=1") + end +end diff --git a/spec/config_spec.rb b/spec/config_spec.rb new file mode 100644 index 0000000..33e59cc --- /dev/null +++ b/spec/config_spec.rb @@ -0,0 +1,42 @@ +require "spec_helper" + +describe ::Linr::Config do + describe "no options" do + subject { ::Linr::Config.new } + + it "has defaults" do + subject.host.must_equal "127.0.0.1" + subject.port.must_equal 8089 + end + end + + describe "with options" do + subject { ::Linr::Config.new(options) } + + describe "minimal options" do + let(:options) { {} } + + it "has defaults" do + subject.host.must_equal "127.0.0.1" + subject.port.must_equal 8089 + end + end + + describe "maximal options" do + let(:options) do + { + host: "localhost", + port: 1337, + logger: logger + } + end + let(:logger) { Logger.new($stdout) } + + it "holds config" do + subject.host.must_equal "localhost" + subject.port.must_equal 1337 + subject.logger.must_equal logger + end + end + end +end diff --git a/spec/connection/udp_spec.rb b/spec/connection/udp_spec.rb index 5d71dc2..62210d7 100644 --- a/spec/connection/udp_spec.rb +++ b/spec/connection/udp_spec.rb @@ -10,6 +10,10 @@ socket.bind(host, port) end + after do + socket.close + end + it "sends data as passed" do payload = "measurement,foo=bar,bat=baz value=12,otherval=21 1439587925" subject.send(payload) diff --git a/spec/logger/null_spec.rb b/spec/logger/null_spec.rb new file mode 100644 index 0000000..3a8fd3c --- /dev/null +++ b/spec/logger/null_spec.rb @@ -0,0 +1,10 @@ +require "spec_helper" + +describe ::Linr::Logger::Null do + subject { ::Linr::Logger::Null.new } + + it "does nothing" do + subject.debug "foo" + subject.add "bar" + end +end