tracing: add opentracing for rack and dyndnsd with configurable jaeger-client support and spanmanager

This commit is contained in:
cn 2018-01-26 16:25:15 +01:00
parent 21857959b5
commit 8d4e96a1dd
9 changed files with 132 additions and 28 deletions

View File

@ -9,6 +9,7 @@ IMPROVEMENTS:
- Better code maintainability by refactorings - Better code maintainability by refactorings
- Update dependencies, mainly `rack` to new major version 2 - Update dependencies, mainly `rack` to new major version 2
- Add Ruby 2.5 support - 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) ## 1.6.1 (October 31, 2017)

View File

@ -168,6 +168,44 @@ users:
password: "ihavenohosts" 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 ## License
dyndnsd.rb is licensed under the Apache License, Version 2.0. See LICENSE for more information. dyndnsd.rb is licensed under the Apache License, Version 2.0. See LICENSE for more information.

View File

@ -23,6 +23,10 @@ Gem::Specification.new do |s|
s.add_runtime_dependency 'rack', '~> 2.0' s.add_runtime_dependency 'rack', '~> 2.0'
s.add_runtime_dependency 'json' s.add_runtime_dependency 'json'
s.add_runtime_dependency 'metriks' 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 'bundler'
s.add_development_dependency 'rake' s.add_development_dependency 'rake'

View File

@ -8,6 +8,9 @@ require 'yaml'
require 'rack' require 'rack'
require 'metriks' require 'metriks'
require 'metriks/reporter/graphite' require 'metriks/reporter/graphite'
require 'opentracing'
require 'rack/tracer'
require 'spanmanager'
require 'dyndnsd/generator/bind' require 'dyndnsd/generator/bind'
require 'dyndnsd/updater/command_with_bind_zone' require 'dyndnsd/updater/command_with_bind_zone'
@ -49,13 +52,17 @@ module Dyndnsd
end end
def authorized?(username, password) def authorized?(username, password)
allow = ((@users.key? username) && (@users[username]['password'] == password)) Helper.span('check_authorized') do |span|
span.set_tag('dyndnsd.user', username)
allow = Helper.user_allowed?(username, password, @users)
if !allow if !allow
Dyndnsd.logger.warn "Login failed for #{username}" Dyndnsd.logger.warn "Login failed for #{username}"
Metriks.meter('requests.auth_failed').mark Metriks.meter('requests.auth_failed').mark
end end
allow allow
end end
end
def call(env) def call(env)
return [422, {'X-DynDNS-Response' => 'method_forbidden'}, []] if env['REQUEST_METHOD'] != 'GET' return [422, {'X-DynDNS-Response' => 'method_forbidden'}, []] if env['REQUEST_METHOD'] != 'GET'
@ -95,6 +102,8 @@ module Dyndnsd
setup_monitoring(config) setup_monitoring(config)
setup_tracing(config)
setup_rack(config) setup_rack(config)
end end
@ -127,9 +136,12 @@ module Dyndnsd
def process_changes(hostnames, myips) def process_changes(hostnames, myips)
changes = [] changes = []
Helper.span('process_changes') do |span|
span.set_tag('dyndnsd.hostnames', hostnames.join(','))
hostnames.each do |hostname| hostnames.each do |hostname|
# myips order is always deterministic # myips order is always deterministic
if (!@db['hosts'].include? hostname) || (@db['hosts'][hostname] != myips) if Helper.changed?(hostname, myips, @db['hosts'])
@db['hosts'][hostname] = myips @db['hosts'][hostname] = myips
changes << :good changes << :good
Metriks.meter('requests.good').mark Metriks.meter('requests.good').mark
@ -138,6 +150,7 @@ module Dyndnsd
Metriks.meter('requests.nochg').mark Metriks.meter('requests.nochg').mark
end end
end end
end
changes changes
end end
@ -158,7 +171,7 @@ module Dyndnsd
hostnames = params['hostname'].split(',') hostnames = params['hostname'].split(',')
# check for invalid hostnames # 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? return [422, {'X-DynDNS-Response' => 'hostname_malformed'}, []] if invalid_hostnames.any?
user = env['REMOTE_USER'] user = env['REMOTE_USER']
@ -227,6 +240,22 @@ module Dyndnsd
end end
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) private_class_method def self.setup_rack(config)
# configure daemon # configure daemon
db = Database.new(config['db']) db = Database.new(config['db'])
@ -242,6 +271,9 @@ module Dyndnsd
app = Responder::DynDNSStyle.new(app) app = Responder::DynDNSStyle.new(app)
end 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'] Rack::Handler::WEBrick.run app, Host: config['host'], Port: config['port']
end end
end end

View File

@ -21,9 +21,11 @@ module Dyndnsd
end end
def save def save
Helper.span('database_save') do |_span|
File.open(@db_file, 'w') { |f| JSON.dump(@db, f) } File.open(@db_file, 'w') { |f| JSON.dump(@db, f) }
@db_hash = @db.hash @db_hash = @db.hash
end end
end
def changed? def changed?
@db_hash != @db.hash @db_hash != @db.hash

View File

@ -10,15 +10,15 @@ module Dyndnsd
@additional_zone_content = config['additional_zone_content'] @additional_zone_content = config['additional_zone_content']
end end
def generate(zone) def generate(db)
out = [] out = []
out << "$TTL #{@ttl}" out << "$TTL #{@ttl}"
out << "$ORIGIN #{@domain}." out << "$ORIGIN #{@domain}."
out << '' 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 << "@ IN NS #{@dns}"
out << '' out << ''
zone['hosts'].each do |hostname, ips| db['hosts'].each do |hostname, ips|
ips.each do |ip| ips.each do |ip|
ip = IPAddr.new(ip).native ip = IPAddr.new(ip).native
type = ip.ipv6? ? 'AAAA' : 'A' type = ip.ipv6? ? 'AAAA' : 'A'

View File

@ -17,5 +17,25 @@ module Dyndnsd
rescue ArgumentError rescue ArgumentError
return false return false
end 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
end end

View File

@ -9,15 +9,20 @@ module Dyndnsd
end end
def update(zone) def update(zone)
Helper.span('updater_update') do |span|
span.set_tag('dyndnsd.updater.name', self.class.name.split('::').last)
# write zone file in bind syntax # write zone file in bind syntax
File.open(@zone_file, 'w') { |f| f.write(@generator.generate(zone)) } File.open(@zone_file, 'w') { |f| f.write(@generator.generate(zone)) }
# call user-defined command # call user-defined command
pid = fork do pid = fork do
exec @command exec @command
end end
# detach so children don't become zombies # detach so children don't become zombies
Process.detach(pid) Process.detach(pid)
end end
end end
end end
end end
end

View File

@ -22,7 +22,9 @@ describe Dyndnsd::Daemon do
app = Rack::Auth::Basic.new(daemon, 'DynDNS', &daemon.method(:authorized?)) 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 end
it 'requires authentication' do it 'requires authentication' do