diff --git a/CHANGELOG.md b/CHANGELOG.md index eb72e30..2a2d6f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ IMPROVEMENTS: - Better code maintainability by refactorings - Update dependencies, mainly `rack` to new major version 2 - Add Ruby 2.5 support +- Add experimental [OpenTracing](http://opentracing.io/) support with [CNCF Jaeger](https://github.com/jaegertracing/jaeger) ## 1.6.1 (October 31, 2017) diff --git a/README.md b/README.md index b34f473..b6ab72a 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,44 @@ users: password: "ihavenohosts" ``` +### Tracing (experimental) + +For tracing dyndnsd.rb is instrumented using the [OpenTracing](http://opentracing.io/) framework and will emit span tracing data for the most important operations happening during the request/response cycle. Using a middleware for Rack allows handling incoming OpenTracing span information properly. +Currently only one OpenTracing-compatible tracer implementation named [CNCF Jaeger](https://github.com/jaegertracing/jaeger) can be configured to use with dyndnsd.rb. + +```yaml +host: "0.0.0.0" +port: "8245" # the DynDNS.com alternative HTTP port +db: "/opt/dyndnsd/db.json" +domain: "dyn.example.org" +# enable and configure tracing using the (currently only) tracer jaeger +tracing: + trust_incoming_span: false # default value, change to accept incoming OpenTracing spans as parents + jaeger: + host: 127.0.0.1 # defaults for host and port of local jaeger-agent + port: 6831 + service_name: "my.dyndnsd.identifier" +# configure the updater, here we use command_with_bind_zone, params are updater-specific +updater: + name: "command_with_bind_zone" + params: + zone_file: "dyn.zone" + command: "echo 'Hello'" + ttl: "5m" + dns: "dns.example.org." + email_addr: "admin.example.org." +# user database with hostnames a user is allowed to update +users: + # 'foo' is username, 'secret' the password + foo: + password: "secret" + hosts: + - foo.example.org + - bar.example.org + test: + password: "ihavenohosts" +``` + ## License dyndnsd.rb is licensed under the Apache License, Version 2.0. See LICENSE for more information. diff --git a/dyndnsd.gemspec b/dyndnsd.gemspec index 27fd10c..0b74b75 100644 --- a/dyndnsd.gemspec +++ b/dyndnsd.gemspec @@ -23,6 +23,10 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'rack', '~> 2.0' s.add_runtime_dependency 'json' s.add_runtime_dependency 'metriks' + s.add_runtime_dependency 'opentracing', '~> 0.3' + s.add_runtime_dependency 'rack-tracer', '~> 0.4' + s.add_runtime_dependency 'spanmanager', '~> 0.3' + s.add_runtime_dependency 'jaeger-client', '~> 0.4' s.add_development_dependency 'bundler' s.add_development_dependency 'rake' diff --git a/lib/dyndnsd.rb b/lib/dyndnsd.rb index 3910f07..2b73e75 100755 --- a/lib/dyndnsd.rb +++ b/lib/dyndnsd.rb @@ -8,6 +8,9 @@ require 'yaml' require 'rack' require 'metriks' require 'metriks/reporter/graphite' +require 'opentracing' +require 'rack/tracer' +require 'spanmanager' require 'dyndnsd/generator/bind' require 'dyndnsd/updater/command_with_bind_zone' @@ -49,12 +52,16 @@ module Dyndnsd end def authorized?(username, password) - allow = ((@users.key? username) && (@users[username]['password'] == password)) - if !allow - Dyndnsd.logger.warn "Login failed for #{username}" - Metriks.meter('requests.auth_failed').mark + Helper.span('check_authorized') do |span| + span.set_tag('dyndnsd.user', username) + + allow = Helper.user_allowed?(username, password, @users) + if !allow + Dyndnsd.logger.warn "Login failed for #{username}" + Metriks.meter('requests.auth_failed').mark + end + allow end - allow end def call(env) @@ -95,6 +102,8 @@ module Dyndnsd setup_monitoring(config) + setup_tracing(config) + setup_rack(config) end @@ -127,15 +136,19 @@ module Dyndnsd def process_changes(hostnames, myips) changes = [] - hostnames.each do |hostname| - # myips order is always deterministic - if (!@db['hosts'].include? hostname) || (@db['hosts'][hostname] != myips) - @db['hosts'][hostname] = myips - changes << :good - Metriks.meter('requests.good').mark - else - changes << :nochg - Metriks.meter('requests.nochg').mark + Helper.span('process_changes') do |span| + span.set_tag('dyndnsd.hostnames', hostnames.join(',')) + + hostnames.each do |hostname| + # myips order is always deterministic + if Helper.changed?(hostname, myips, @db['hosts']) + @db['hosts'][hostname] = myips + changes << :good + Metriks.meter('requests.good').mark + else + changes << :nochg + Metriks.meter('requests.nochg').mark + end end end changes @@ -158,7 +171,7 @@ module Dyndnsd hostnames = params['hostname'].split(',') # check for invalid hostnames - invalid_hostnames = hostnames.select { |hostname| !Helper.fqdn_valid?(hostname, @domain) } + invalid_hostnames = hostnames.select { |h| !Helper.fqdn_valid?(h, @domain) } return [422, {'X-DynDNS-Response' => 'hostname_malformed'}, []] if invalid_hostnames.any? user = env['REMOTE_USER'] @@ -227,6 +240,22 @@ module Dyndnsd end end + private_class_method def self.setup_tracing(config) + # configure OpenTracing + if config.dig('tracing', 'jaeger') + require 'jaeger/client' + + host = config['tracing']['jaeger']['host'] || '127.0.0.1' + port = config['tracing']['jaeger']['port'] || 6831 + service_name = config['tracing']['jaeger']['service_name'] || 'dyndnsd' + OpenTracing.global_tracer = Jaeger::Client.build( + host: host, port: port, service_name: service_name, flush_interval: 1 + ) + end + # always use SpanManager + OpenTracing.global_tracer = SpanManager::Tracer.new(OpenTracing.global_tracer) + end + private_class_method def self.setup_rack(config) # configure daemon db = Database.new(config['db']) @@ -242,6 +271,9 @@ module Dyndnsd app = Responder::DynDNSStyle.new(app) end + trust_incoming_span = config.dig('tracing', 'trust_incoming_span') || false + app = Rack::Tracer.new(app, trust_incoming_span: trust_incoming_span) + Rack::Handler::WEBrick.run app, Host: config['host'], Port: config['port'] end end diff --git a/lib/dyndnsd/database.rb b/lib/dyndnsd/database.rb index eb88106..7ae6c21 100644 --- a/lib/dyndnsd/database.rb +++ b/lib/dyndnsd/database.rb @@ -21,8 +21,10 @@ module Dyndnsd end def save - File.open(@db_file, 'w') { |f| JSON.dump(@db, f) } - @db_hash = @db.hash + Helper.span('database_save') do |_span| + File.open(@db_file, 'w') { |f| JSON.dump(@db, f) } + @db_hash = @db.hash + end end def changed? diff --git a/lib/dyndnsd/generator/bind.rb b/lib/dyndnsd/generator/bind.rb index 66e2368..213ff85 100644 --- a/lib/dyndnsd/generator/bind.rb +++ b/lib/dyndnsd/generator/bind.rb @@ -10,15 +10,15 @@ module Dyndnsd @additional_zone_content = config['additional_zone_content'] end - def generate(zone) + def generate(db) out = [] out << "$TTL #{@ttl}" out << "$ORIGIN #{@domain}." out << '' - out << "@ IN SOA #{@dns} #{@email_addr} ( #{zone['serial']} 3h 5m 1w 1h )" + out << "@ IN SOA #{@dns} #{@email_addr} ( #{db['serial']} 3h 5m 1w 1h )" out << "@ IN NS #{@dns}" out << '' - zone['hosts'].each do |hostname, ips| + db['hosts'].each do |hostname, ips| ips.each do |ip| ip = IPAddr.new(ip).native type = ip.ipv6? ? 'AAAA' : 'A' diff --git a/lib/dyndnsd/helper.rb b/lib/dyndnsd/helper.rb index 74de88e..9eb738d 100644 --- a/lib/dyndnsd/helper.rb +++ b/lib/dyndnsd/helper.rb @@ -17,5 +17,25 @@ module Dyndnsd rescue ArgumentError return false end + + def self.user_allowed?(username, password, users) + (users.key? username) && (users[username]['password'] == password) + end + + def self.changed?(hostname, myips, hosts) + # myips order is always deterministic + (!hosts.include? hostname) || (hosts[hostname] != myips) + end + + def self.span(operation, &block) + span = OpenTracing.start_span(operation) + span.set_tag('component', 'dyndnsd') + span.set_tag('span.kind', 'server') + begin + block.call(span) + ensure + span.finish + end + end end end diff --git a/lib/dyndnsd/updater/command_with_bind_zone.rb b/lib/dyndnsd/updater/command_with_bind_zone.rb index a7d1aae..11e23b4 100644 --- a/lib/dyndnsd/updater/command_with_bind_zone.rb +++ b/lib/dyndnsd/updater/command_with_bind_zone.rb @@ -9,14 +9,19 @@ module Dyndnsd end def update(zone) - # write zone file in bind syntax - File.open(@zone_file, 'w') { |f| f.write(@generator.generate(zone)) } - # call user-defined command - pid = fork do - exec @command + Helper.span('updater_update') do |span| + span.set_tag('dyndnsd.updater.name', self.class.name.split('::').last) + + # write zone file in bind syntax + File.open(@zone_file, 'w') { |f| f.write(@generator.generate(zone)) } + # call user-defined command + pid = fork do + exec @command + end + + # detach so children don't become zombies + Process.detach(pid) end - # detach so children don't become zombies - Process.detach(pid) end end end diff --git a/spec/daemon_spec.rb b/spec/daemon_spec.rb index 2fcbc7e..f287099 100644 --- a/spec/daemon_spec.rb +++ b/spec/daemon_spec.rb @@ -22,7 +22,9 @@ describe Dyndnsd::Daemon do app = Rack::Auth::Basic.new(daemon, 'DynDNS', &daemon.method(:authorized?)) - Dyndnsd::Responder::DynDNSStyle.new(app) + app = Dyndnsd::Responder::DynDNSStyle.new(app) + + Rack::Tracer.new(app, trust_incoming_span: false) end it 'requires authentication' do