diff --git a/.env.sample b/.env.sample index 7edea10b9..b00d264b2 100644 --- a/.env.sample +++ b/.env.sample @@ -50,7 +50,9 @@ # The application must have its OAuth2 redirect URI set to ${JOINER_BASE_URL}/callback. # You also need to fill out all the JOINER_* environment variables below. # -# COMPOSE_PROFILES=discord +# datadog: Start the datadog agent to push performance metrics through ddtrace. +# You also need to fill out the DD_API_KEY environment variables below. +# COMPOSE_PROFILES=discord,datadog # Change the ports that are forwarded by docker to avoid potential conflicts @@ -64,3 +66,13 @@ # JOINER_OAUTH2_CLIENT_SECRET= # JOINER_GUILD_ID= # JOINER_FAILED_JOIN_WEBHOOK_URL= + +# The following environment variables are used when using the 'datadog' profile: + +# Required: +# DD_API_KEY= +# +# Optional: +# DD_SITE=us5.datadoghq.com +# DD_SERVICE=E6ng (Dev) +# DD_ENV=local diff --git a/Gemfile b/Gemfile index 29d0d8b61..468a0c105 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,8 @@ gem "rugged" # Blocked by unicorn which lacks a release with Rack 3 support gem "rack", "~> 2.0" +gem "datadog", require: "datadog/auto_instrument" + gem 'opensearch-ruby' gem 'mailgun-ruby' diff --git a/Gemfile.lock b/Gemfile.lock index d90902f2b..192d6bba1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -115,7 +115,14 @@ GEM rexml crass (1.0.6) dalli (3.2.6) + datadog (2.0.0.beta1) + base64 + debase-ruby_core_source (= 3.3.1) + libdatadog (~> 6.0.0.2.0) + libddwaf (~> 1.14.0.0.0) + msgpack date (3.3.4) + debase-ruby_core_source (3.3.1) debug (1.9.1) irb (~> 1.10) reline (>= 0.3.8) @@ -163,6 +170,9 @@ GEM jsonapi-renderer (0.2.2) kgio (2.11.4) language_server-protocol (3.17.0.3) + libdatadog (6.0.0.2.0) + libddwaf (1.14.0.0.0) + ffi (~> 1.0) listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -380,6 +390,7 @@ DEPENDENCIES bcrypt bootsnap dalli + datadog debug diffy dotenv diff --git a/app/logical/danbooru_logger.rb b/app/logical/danbooru_logger.rb index 4ce6e740b..9fc69e843 100644 --- a/app/logical/danbooru_logger.rb +++ b/app/logical/danbooru_logger.rb @@ -1,20 +1,22 @@ # frozen_string_literal: true class DanbooruLogger - def self.log(exception, expected: false, **_params) + def self.log(exception, expected: false) if expected Rails.logger.info("#{exception.class}: #{exception.message}") else backtrace = Rails.backtrace_cleaner.clean(exception.backtrace).join("\n") Rails.logger.error("#{exception.class}: #{exception.message}\n#{backtrace}") end + + Datadog::Tracing.active_span&.set_error(exception) unless expected end def self.initialize(user) - add_attributes("user.id" => user.id, "user.name" => user.name) + add_attributes("user.id" => user.id) unless user.is_anonymous? end def self.add_attributes(**) - # noop + Datadog::Tracing.active_span&.set_tags(**) end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 091dd5f5c..cfdefcd19 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -217,8 +217,7 @@ class ApplicationRecord < ActiveRecord::Base def with_timeout(n, default_value = nil) connection.execute("SET STATEMENT_TIMEOUT = #{n}") unless Rails.env == "test" yield - rescue ::ActiveRecord::StatementInvalid => x - DanbooruLogger.log(x, expected: true) + rescue ::ActiveRecord::StatementInvalid return default_value ensure connection.execute("SET STATEMENT_TIMEOUT = #{CurrentUser.user.try(:statement_timeout) || 3_000}") unless Rails.env == "test" diff --git a/app/models/tag_alias.rb b/app/models/tag_alias.rb index 67e9cfc26..f4b112099 100644 --- a/app/models/tag_alias.rb +++ b/app/models/tag_alias.rb @@ -206,8 +206,6 @@ class TagAlias < TagRelationship forum_updater.update(failure_message(e), "FAILED") if update_topic update_columns(status: "error: #{e}") end - - DanbooruLogger.log(e, tag_alias_id: id, antecedent_name: antecedent_name, consequent_name: consequent_name) end end diff --git a/app/models/tag_implication.rb b/app/models/tag_implication.rb index 9c74539f8..444992cc3 100644 --- a/app/models/tag_implication.rb +++ b/app/models/tag_implication.rb @@ -138,8 +138,6 @@ class TagImplication < TagRelationship forum_updater.update(failure_message(e), "FAILED") if update_topic update_columns(status: "error: #{e}") - - DanbooruLogger.log(e, tag_implication_id: id, antecedent_name: antecedent_name, consequent_name: consequent_name) end end diff --git a/config/initializers/01_sensitive_params.rb b/config/initializers/01_sensitive_params.rb new file mode 100644 index 000000000..30944b7f3 --- /dev/null +++ b/config/initializers/01_sensitive_params.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module SensitiveParams + # Common values for Rails and Datadog query parameter filtering + PARAMS = %i[passw secret token _key crypt salt certificate otp ssn email].freeze + + def self.to_datadog_regex + Regexp.new("(?:#{PARAMS.join('|')})[^&]*=[^&]*") + end +end diff --git a/config/initializers/datadog.rb b/config/initializers/datadog.rb new file mode 100644 index 000000000..40a465a57 --- /dev/null +++ b/config/initializers/datadog.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +Datadog.configure do |c| + c.tracing.enabled = !Rails.env.test? && ENV["DD_API_KEY"].present? + c.logger.level = Logger::WARN + + c.tracing.instrument :rack, quantize: { + query: { + obfuscate: { + regex: SensitiveParams.to_datadog_regex, + }, + }, + } +end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index f11e977cc..161cca8b1 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -5,6 +5,4 @@ # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. # Use this to limit dissemination of sensitive information. # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. -Rails.application.config.filter_parameters += [ - :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :email -] +Rails.application.config.filter_parameters += SensitiveParams::PARAMS diff --git a/docker-compose.yml b/docker-compose.yml index 29ce2a8e8..8dc62b9e1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ x-environment: &common-env SESSION_SECRET_KEY: 44b4f44e9f253c406cbe727d403d500c1cecff943e4d2aea8f5447f28846fffe # Hide annoying output from libvips on corrupt files VIPS_WARNING: "0" + DD_TRACE_STARTUP_LOGS: false x-depends-on: &common-depends-on opensearch: @@ -43,6 +44,9 @@ services: environment: <<: *common-env RAILS_ENV: development + DD_AGENT_HOST: datadog-agent + DD_SERVICE: ${DD_SERVICE:-} + DD_ENV: ${DD_ENV:-} depends_on: <<: *common-depends-on autocompleted: @@ -118,6 +122,17 @@ services: command: iqdb http 0.0.0.0 5588 /iqdb/e621_v2.db volumes: - iqdb_data:/iqdb + + datadog-agent: + image: datadog/agent:7.52.0 + environment: + DD_API_KEY: ${DD_API_KEY:-} + DD_SITE: ${DD_SITE:-us5.datadoghq.com} + DD_HOSTNAME: datadog-agent + DD_LOG_LEVEL: WARN + DD_APM_NON_LOCAL_TRAFFIC: true + profiles: + - datadog # Discord integration diff --git a/test/unit/datadog_test.rb b/test/unit/datadog_test.rb new file mode 100644 index 000000000..9afba546f --- /dev/null +++ b/test/unit/datadog_test.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "test_helper" + +class DatadogTest < ActiveSupport::TestCase + def assert_match_group(expected, query) + assert_equal(expected, query[SensitiveParams.to_datadog_regex]) + end + + def assert_no_match_group(query) + assert_nil(query[SensitiveParams.to_datadog_regex]) + end + + should "filters query parameters" do + assert_match_group("password=hunter2", "password=hunter2") + assert_match_group("password=hunter2", "?abc=def&password=hunter2&foo=bar") + # These partial matches are fine, just don't let datadog grab it + assert_match_group("password]=hunter2", "?abc=def&user[password]=hunter2&foo=bar") + assert_match_group("password=hunter2", "old_password=hunter2") + assert_match_group("password_old=hunter2", "password_old=hunter2") + + assert_no_match_group("something=else") + assert_no_match_group("search[foo]=bar") + end +end