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,12 +52,16 @@ 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|
if !allow span.set_tag('dyndnsd.user', username)
Dyndnsd.logger.warn "Login failed for #{username}"
Metriks.meter('requests.auth_failed').mark 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 end
allow
end end
def call(env) def call(env)
@ -95,6 +102,8 @@ module Dyndnsd
setup_monitoring(config) setup_monitoring(config)
setup_tracing(config)
setup_rack(config) setup_rack(config)
end end
@ -127,15 +136,19 @@ module Dyndnsd
def process_changes(hostnames, myips) def process_changes(hostnames, myips)
changes = [] changes = []
hostnames.each do |hostname| Helper.span('process_changes') do |span|
# myips order is always deterministic span.set_tag('dyndnsd.hostnames', hostnames.join(','))
if (!@db['hosts'].include? hostname) || (@db['hosts'][hostname] != myips)
@db['hosts'][hostname] = myips hostnames.each do |hostname|
changes << :good # myips order is always deterministic
Metriks.meter('requests.good').mark if Helper.changed?(hostname, myips, @db['hosts'])
else @db['hosts'][hostname] = myips
changes << :nochg changes << :good
Metriks.meter('requests.nochg').mark Metriks.meter('requests.good').mark
else
changes << :nochg
Metriks.meter('requests.nochg').mark
end
end end
end end
changes changes
@ -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,8 +21,10 @@ module Dyndnsd
end end
def save def save
File.open(@db_file, 'w') { |f| JSON.dump(@db, f) } Helper.span('database_save') do |_span|
@db_hash = @db.hash File.open(@db_file, 'w') { |f| JSON.dump(@db, f) }
@db_hash = @db.hash
end
end end
def changed? def changed?

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