From e461c91627eb41b17111cc2d24840d5ae6745f64 Mon Sep 17 00:00:00 2001 From: Alex Evanczuk Date: Thu, 18 Aug 2022 07:37:32 -0400 Subject: [PATCH 1/4] move files into own class --- .../private/datadog_reporter.rb | 26 +++------- .../private/metrics.rb | 16 ++++++ .../private/metrics/files.rb | 51 +++++++++++++++++++ 3 files changed, 73 insertions(+), 20 deletions(-) create mode 100644 lib/modularization_statistics/private/metrics.rb create mode 100644 lib/modularization_statistics/private/metrics/files.rb diff --git a/lib/modularization_statistics/private/datadog_reporter.rb b/lib/modularization_statistics/private/datadog_reporter.rb index 4f6390d..28c5c83 100644 --- a/lib/modularization_statistics/private/datadog_reporter.rb +++ b/lib/modularization_statistics/private/datadog_reporter.rb @@ -2,14 +2,14 @@ # frozen_string_literal: true require 'dogapi' +require 'modularization_statistics/private/metrics' +require 'modularization_statistics/private/metrics/files' module ModularizationStatistics module Private class DatadogReporter extend T::Sig - UNKNOWN_OWNER = T.let('Unknown', String) - sig do params( source_code_files: T::Array[SourceCodeFile], @@ -18,16 +18,7 @@ class DatadogReporter end def self.get_metrics(source_code_files:, app_name:) all_metrics = T.let([], T::Array[GaugeMetric]) - app_level_tag = Tag.for('app', app_name) - - source_code_files.group_by { |file| file.team_owner&.name }.each do |team_name, files_for_team| - file_tags = tags_for_team(team_name) + [app_level_tag] - all_metrics += get_file_metrics('by_team', file_tags, files_for_team) - end - - file_tags = [app_level_tag] - all_metrics += get_file_metrics('totals', file_tags, source_code_files) - + all_metrics += Metrics::Files.get_metrics(source_code_files, app_name) packages = ParsePackwerk.all all_metrics += get_package_metrics(packages, app_name) all_metrics += get_package_metrics_by_team(packages, app_name) @@ -64,18 +55,13 @@ def self.tags_for_package(package, app_name) [ Tag.new(key: 'package', value: humanized_package_name(package.name)), Tag.new(key: 'app', value: app_name), - *tags_for_team(CodeOwnership.for_package(package)&.name), + *Metrics.tags_for_team(CodeOwnership.for_package(package)&.name), ] end - sig { params(team_name: T.nilable(String)).returns(T::Array[Tag]) } - def self.tags_for_team(team_name) - [Tag.for('team', team_name || UNKNOWN_OWNER)] - end - sig { params(team_name: T.nilable(String)).returns(T::Array[Tag]) } def self.tags_for_to_team(team_name) - [Tag.for('to_team', team_name || UNKNOWN_OWNER)] + [Tag.for('to_team', team_name || Metrics::UNKNOWN_OWNER)] end private_class_method :tags_for_package @@ -191,7 +177,7 @@ def self.get_package_metrics_by_team(all_packages, app_name) # We look at `all_packages` because we care about ALL inbound violations across all teams inbound_violations_by_package = all_protected_packages.flat_map(&:violations).group_by(&:to_package_name) - team_tags = tags_for_team(team_name) + [app_level_tag] + team_tags = Metrics.tags_for_team(team_name) + [app_level_tag] all_metrics << GaugeMetric.for('by_team.all_packages.count', protected_packages_by_team.count, team_tags) all_metrics += self.get_protections_metrics('by_team', protected_packages_by_team, team_tags) all_metrics += self.get_public_usage_metrics('by_team', protected_packages_by_team.map(&:original_package), team_tags) diff --git a/lib/modularization_statistics/private/metrics.rb b/lib/modularization_statistics/private/metrics.rb new file mode 100644 index 0000000..879c2a7 --- /dev/null +++ b/lib/modularization_statistics/private/metrics.rb @@ -0,0 +1,16 @@ +# typed: strict +# frozen_string_literal: true + +module ModularizationStatistics + module Private + module Metrics + extend T::Sig + UNKNOWN_OWNER = T.let('Unknown', String) + + sig { params(team_name: T.nilable(String)).returns(T::Array[Tag]) } + def self.tags_for_team(team_name) + [Tag.for('team', team_name || UNKNOWN_OWNER)] + end + end + end +end diff --git a/lib/modularization_statistics/private/metrics/files.rb b/lib/modularization_statistics/private/metrics/files.rb new file mode 100644 index 0000000..bae33bd --- /dev/null +++ b/lib/modularization_statistics/private/metrics/files.rb @@ -0,0 +1,51 @@ +# typed: strict +# frozen_string_literal: true + +require 'dogapi' + +module ModularizationStatistics + module Private + module Metrics + class Files + extend T::Sig + + UNKNOWN_OWNER = T.let('Unknown', String) + + sig do + params( + source_code_files: T::Array[SourceCodeFile], + app_name: String + ).returns(T::Array[GaugeMetric]) + end + def self.get_metrics(source_code_files, app_name) + all_metrics = T.let([], T::Array[GaugeMetric]) + app_level_tag = Tag.for('app', app_name) + + source_code_files.group_by { |file| file.team_owner&.name }.each do |team_name, files_for_team| + file_tags = Metrics.tags_for_team(team_name) + [app_level_tag] + all_metrics += get_file_metrics('by_team', file_tags, files_for_team) + end + + file_tags = [app_level_tag] + all_metrics += get_file_metrics('totals', file_tags, source_code_files) + all_metrics + end + + sig do + params( + metric_name_suffix: String, + tags: T::Array[Tag], + files: T::Array[SourceCodeFile] + ).returns(T::Array[GaugeMetric]) + end + def self.get_file_metrics(metric_name_suffix, tags, files) + [ + GaugeMetric.for("component_files.#{metric_name_suffix}", files.count(&:componentized_file?), tags), + GaugeMetric.for("packaged_files.#{metric_name_suffix}", files.count(&:packaged_file?), tags), + GaugeMetric.for("all_files.#{metric_name_suffix}", files.count, tags), + ] + end + end + end + end +end From 13a6d14d2167e7ad2009773bac30d07df59fe35d Mon Sep 17 00:00:00 2001 From: Alex Evanczuk Date: Thu, 18 Aug 2022 07:44:56 -0400 Subject: [PATCH 2/4] move packages into its own class --- .../private/datadog_reporter.rb | 225 ++---------------- .../private/metrics.rb | 33 +++ .../private/metrics/files.rb | 4 - .../private/metrics/packages.rb | 109 +++++++++ .../private/metrics/protection_usage.rb | 33 +++ .../private/metrics/public_usage.rb | 36 +++ 6 files changed, 227 insertions(+), 213 deletions(-) create mode 100644 lib/modularization_statistics/private/metrics/packages.rb create mode 100644 lib/modularization_statistics/private/metrics/protection_usage.rb create mode 100644 lib/modularization_statistics/private/metrics/public_usage.rb diff --git a/lib/modularization_statistics/private/datadog_reporter.rb b/lib/modularization_statistics/private/datadog_reporter.rb index 28c5c83..3335056 100644 --- a/lib/modularization_statistics/private/datadog_reporter.rb +++ b/lib/modularization_statistics/private/datadog_reporter.rb @@ -4,6 +4,9 @@ require 'dogapi' require 'modularization_statistics/private/metrics' require 'modularization_statistics/private/metrics/files' +require 'modularization_statistics/private/metrics/public_usage' +require 'modularization_statistics/private/metrics/protection_usage' +require 'modularization_statistics/private/metrics/packages' module ModularizationStatistics module Private @@ -20,7 +23,7 @@ def self.get_metrics(source_code_files:, app_name:) all_metrics = T.let([], T::Array[GaugeMetric]) all_metrics += Metrics::Files.get_metrics(source_code_files, app_name) packages = ParsePackwerk.all - all_metrics += get_package_metrics(packages, app_name) + all_metrics += Metrics::Packages.get_package_metrics(packages, app_name) all_metrics += get_package_metrics_by_team(packages, app_name) all_metrics @@ -50,119 +53,6 @@ def self.report!(datadog_client:, report_time:, metrics:) end end - sig { params(package: ParsePackwerk::Package, app_name: String).returns(T::Array[Tag]) } - def self.tags_for_package(package, app_name) - [ - Tag.new(key: 'package', value: humanized_package_name(package.name)), - Tag.new(key: 'app', value: app_name), - *Metrics.tags_for_team(CodeOwnership.for_package(package)&.name), - ] - end - - sig { params(team_name: T.nilable(String)).returns(T::Array[Tag]) } - def self.tags_for_to_team(team_name) - [Tag.for('to_team', team_name || Metrics::UNKNOWN_OWNER)] - end - - private_class_method :tags_for_package - - sig do - params( - packages: T::Array[ParsePackwerk::Package], - app_name: String - ).returns(T::Array[GaugeMetric]) - end - def self.get_package_metrics(packages, app_name) - all_metrics = [] - app_level_tag = Tag.for('app', app_name) - package_tags = T.let([app_level_tag], T::Array[Tag]) - protected_packages = packages.map { |p| PackageProtections::ProtectedPackage.from(p) } - - all_metrics << GaugeMetric.for('all_packages.count', packages.count, package_tags) - all_metrics << GaugeMetric.for('all_packages.dependencies.count', packages.sum { |package| package.dependencies.count }, package_tags) - all_metrics << GaugeMetric.for('all_packages.dependency_violations.count', protected_packages.sum { |package| file_count(package.violations.select(&:dependency?)) }, package_tags) - all_metrics << GaugeMetric.for('all_packages.privacy_violations.count', protected_packages.sum { |package| file_count(package.violations.select(&:privacy?)) }, package_tags) - all_metrics << GaugeMetric.for('all_packages.enforcing_dependencies.count', packages.count(&:enforces_dependencies?), package_tags) - all_metrics << GaugeMetric.for('all_packages.enforcing_privacy.count', packages.count(&:enforces_privacy?), package_tags) - - all_metrics << GaugeMetric.for('all_packages.notify_on_package_yml_changes.count', packages.count { |p| p.metadata['notify_on_package_yml_changes'] }, package_tags) - all_metrics << GaugeMetric.for('all_packages.notify_on_new_violations.count', packages.count { |p| p.metadata['notify_on_new_violations'] }, package_tags) - - all_metrics << GaugeMetric.for('all_packages.with_violations.count', protected_packages.count { |package| package.violations.any? }, package_tags) - all_metrics += self.get_public_usage_metrics('all_packages', packages, package_tags) - all_metrics << GaugeMetric.for('all_packages.has_readme.count', packages.count { |package| has_readme?(package) }, package_tags) - - all_metrics += self.get_protections_metrics('all_packages', protected_packages, package_tags) - all_metrics << GaugeMetric.for('all_packages.package_based_file_ownership.count', packages.count { |package| !package.metadata['owner'].nil? }, package_tags) - - inbound_violations_by_package = protected_packages.flat_map(&:violations).group_by(&:to_package_name) - - protected_packages.each do |protected_package| - package = protected_package.original_package - package_tags = tags_for_package(package, app_name) - - # - # VIOLATIONS (implicit dependencies) - # - outbound_violations = protected_package.violations - inbound_violations = inbound_violations_by_package[package.name] || [] - all_dependency_violations = (outbound_violations + inbound_violations).select(&:dependency?) - all_privacy_violations = (outbound_violations + inbound_violations).select(&:privacy?) - - all_metrics << GaugeMetric.for('by_package.dependency_violations.count', file_count(all_dependency_violations), package_tags) - all_metrics << GaugeMetric.for('by_package.privacy_violations.count', file_count(all_privacy_violations), package_tags) - - all_metrics << GaugeMetric.for('by_package.outbound_dependency_violations.count', file_count(outbound_violations.select(&:dependency?)), package_tags) - all_metrics << GaugeMetric.for('by_package.inbound_dependency_violations.count', file_count(inbound_violations.select(&:dependency?)), package_tags) - - all_metrics << GaugeMetric.for('by_package.outbound_privacy_violations.count', file_count(outbound_violations.select(&:privacy?)), package_tags) - all_metrics << GaugeMetric.for('by_package.inbound_privacy_violations.count', file_count(inbound_violations.select(&:privacy?)), package_tags) - - all_metrics += self.get_public_usage_metrics('by_package', [package], package_tags) - - protected_package.violations.group_by(&:to_package_name).each do |to_package_name, violations| - to_package = ParsePackwerk.find(to_package_name) - if to_package.nil? - raise StandardError, "Could not find matching package #{to_package_name}" - end - - tags = package_tags + [Tag.for('to_package', humanized_package_name(to_package_name))] + tags_for_to_team(CodeOwnership.for_package(to_package)&.name) - all_metrics << GaugeMetric.for('by_package.outbound_dependency_violations.per_package.count', file_count(violations.select(&:dependency?)), tags) - all_metrics << GaugeMetric.for('by_package.outbound_privacy_violations.per_package.count', file_count(violations.select(&:privacy?)), tags) - end - end - - inbound_explicit_dependency_by_package = {} - packages.each do |package| - package.dependencies.each do |explicit_dependency| - inbound_explicit_dependency_by_package[explicit_dependency] ||= [] - inbound_explicit_dependency_by_package[explicit_dependency] << package.name - end - end - - packages.each do |package| # rubocop:disable Style/CombinableLoops - package_tags = tags_for_package(package, app_name) - - # - # EXPLICIT DEPENDENCIES - # - package.dependencies.each do |explicit_dependency| - to_package = ParsePackwerk.find(explicit_dependency) - if to_package.nil? - raise StandardError, "Could not find matching package #{explicit_dependency}" - end - - tags = package_tags + [Tag.for('to_package', humanized_package_name(explicit_dependency))] + tags_for_to_team(CodeOwnership.for_package(to_package)&.name) - all_metrics << GaugeMetric.for('by_package.outbound_explicit_dependencies.per_package.count', 1, tags) - end - - all_metrics << GaugeMetric.for('by_package.outbound_explicit_dependencies.count', package.dependencies.count, package_tags) - all_metrics << GaugeMetric.for('by_package.inbound_explicit_dependencies.count', inbound_explicit_dependency_by_package[package.name]&.count || 0, package_tags) - end - - all_metrics - end - sig do params( all_packages: T::Array[ParsePackwerk::Package], @@ -179,8 +69,8 @@ def self.get_package_metrics_by_team(all_packages, app_name) team_tags = Metrics.tags_for_team(team_name) + [app_level_tag] all_metrics << GaugeMetric.for('by_team.all_packages.count', protected_packages_by_team.count, team_tags) - all_metrics += self.get_protections_metrics('by_team', protected_packages_by_team, team_tags) - all_metrics += self.get_public_usage_metrics('by_team', protected_packages_by_team.map(&:original_package), team_tags) + all_metrics += Metrics::ProtectionUsage.get_protections_metrics('by_team', protected_packages_by_team, team_tags) + all_metrics += Metrics::PublicUsage.get_public_usage_metrics('by_team', protected_packages_by_team.map(&:original_package), team_tags) all_metrics << GaugeMetric.for('by_team.notify_on_package_yml_changes.count', protected_packages_by_team.count { |p| p.metadata['notify_on_package_yml_changes'] }, team_tags) all_metrics << GaugeMetric.for('by_team.notify_on_new_violations.count', protected_packages_by_team.count { |p| p.metadata['notify_on_new_violations'] }, team_tags) @@ -194,16 +84,16 @@ def self.get_package_metrics_by_team(all_packages, app_name) all_dependency_violations = (outbound_violations + inbound_violations).select(&:dependency?) all_privacy_violations = (outbound_violations + inbound_violations).select(&:privacy?) - all_metrics << GaugeMetric.for('by_team.dependency_violations.count', file_count(all_dependency_violations), team_tags) - all_metrics << GaugeMetric.for('by_team.privacy_violations.count', file_count(all_privacy_violations), team_tags) + all_metrics << GaugeMetric.for('by_team.dependency_violations.count', Metrics.file_count(all_dependency_violations), team_tags) + all_metrics << GaugeMetric.for('by_team.privacy_violations.count', Metrics.file_count(all_privacy_violations), team_tags) - all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.count', file_count(outbound_violations.select(&:dependency?)), team_tags) - all_metrics << GaugeMetric.for('by_team.inbound_dependency_violations.count', file_count(inbound_violations.select(&:dependency?)), team_tags) + all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.count', Metrics.file_count(outbound_violations.select(&:dependency?)), team_tags) + all_metrics << GaugeMetric.for('by_team.inbound_dependency_violations.count', Metrics.file_count(inbound_violations.select(&:dependency?)), team_tags) - all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.count', file_count(outbound_violations.select(&:privacy?)), team_tags) - all_metrics << GaugeMetric.for('by_team.inbound_privacy_violations.count', file_count(inbound_violations.select(&:privacy?)), team_tags) + all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.count', Metrics.file_count(outbound_violations.select(&:privacy?)), team_tags) + all_metrics << GaugeMetric.for('by_team.inbound_privacy_violations.count', Metrics.file_count(inbound_violations.select(&:privacy?)), team_tags) - all_metrics << GaugeMetric.for('by_team.has_readme.count', protected_packages_by_team.count { |protected_package| has_readme?(protected_package.original_package) }, team_tags) + all_metrics << GaugeMetric.for('by_team.has_readme.count', protected_packages_by_team.count { |protected_package| Metrics.has_readme?(protected_package.original_package) }, team_tags) grouped_outbound_violations = outbound_violations.group_by do |violation| to_package = ParsePackwerk.find(violation.to_package_name) @@ -215,97 +105,14 @@ def self.get_package_metrics_by_team(all_packages, app_name) end grouped_outbound_violations.each do |to_team_name, violations| - tags = team_tags + tags_for_to_team(to_team_name) - all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.per_team.count', file_count(violations.select(&:dependency?)), tags) - all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.per_team.count', file_count(violations.select(&:privacy?)), tags) + tags = team_tags + Metrics.tags_for_to_team(to_team_name) + all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.per_team.count', Metrics.file_count(violations.select(&:dependency?)), tags) + all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.per_team.count', Metrics.file_count(violations.select(&:privacy?)), tags) end end all_metrics end - - private_class_method :get_package_metrics - - sig do - params( - metric_name_suffix: String, - tags: T::Array[Tag], - files: T::Array[SourceCodeFile] - ).returns(T::Array[GaugeMetric]) - end - def self.get_file_metrics(metric_name_suffix, tags, files) - [ - GaugeMetric.for("component_files.#{metric_name_suffix}", files.count(&:componentized_file?), tags), - GaugeMetric.for("packaged_files.#{metric_name_suffix}", files.count(&:packaged_file?), tags), - GaugeMetric.for("all_files.#{metric_name_suffix}", files.count, tags), - ] - end - - private_class_method :get_file_metrics - - sig { params(prefix: String, protected_packages: T::Array[PackageProtections::ProtectedPackage], package_tags: T::Array[Tag]).returns(T::Array[GaugeMetric]) } - def self.get_protections_metrics(prefix, protected_packages, package_tags) - PackageProtections.all.flat_map do |protection| - PackageProtections::ViolationBehavior.each_value.map do |violation_behavior| - # https://github.com/Gusto/package_protections/pull/42 changed the public API of these violation behaviors. - # To preserve our ability to understand historical trends, we map to the old values. - # This allows our dashboards to continue to operate as expected. - # Note if we ever open source mod stats, we should probably inject this behavior so that new clients can see the new keys in their metrics. - violation_behavior_map = { - PackageProtections::ViolationBehavior::FailOnAny => 'fail_the_build_on_any_instances', - PackageProtections::ViolationBehavior::FailNever => 'no', - PackageProtections::ViolationBehavior::FailOnNew => 'fail_the_build_if_new_instances_appear', - } - violation_behavior_name = violation_behavior_map[violation_behavior] - metric_name = "#{prefix}.#{protection.identifier}.#{violation_behavior_name}.count" - count_of_packages = protected_packages.count { |p| p.violation_behavior_for(protection.identifier) == violation_behavior } - GaugeMetric.for(metric_name, count_of_packages, package_tags) - end - end - end - - sig { params(prefix: String, packages: T::Array[ParsePackwerk::Package], package_tags: T::Array[Tag]).returns(T::Array[GaugeMetric]) } - def self.get_public_usage_metrics(prefix, packages, package_tags) - packages_except_for_root = packages.reject { |package| package.name == ParsePackwerk::ROOT_PACKAGE_NAME } - all_files = packages_except_for_root.flat_map do |package| - package.directory.glob('**/**.rb') - end - - all_public_files = T.let([], T::Array[Pathname]) - is_using_public_directory = 0 - packages_except_for_root.each do |package| - public_files = package.directory.glob('app/public/**/**.rb') - all_public_files += public_files - is_using_public_directory += 1 if public_files.any? - end - - # In Datadog, can divide public files by all files to get the ratio. - # This is not a metric that we are targeting -- its for observability and reflection only. - [ - GaugeMetric.for("#{prefix}.all_files.count", all_files.count, package_tags), - GaugeMetric.for("#{prefix}.public_files.count", all_public_files.count, package_tags), - GaugeMetric.for("#{prefix}.using_public_directory.count", is_using_public_directory, package_tags), - ] - end - - sig { params(package: ParsePackwerk::Package).returns(T::Boolean) } - def self.has_readme?(package) - package.directory.join('README.md').exist? - end - - sig { params(violations: T::Array[ParsePackwerk::Violation]).returns(Integer) } - def self.file_count(violations) - violations.sum { |v| v.files.count } - end - - sig { params(name: String).returns(String) } - def self.humanized_package_name(name) - if name == ParsePackwerk::ROOT_PACKAGE_NAME - 'root' - else - name - end - end end end end diff --git a/lib/modularization_statistics/private/metrics.rb b/lib/modularization_statistics/private/metrics.rb index 879c2a7..a1bb7eb 100644 --- a/lib/modularization_statistics/private/metrics.rb +++ b/lib/modularization_statistics/private/metrics.rb @@ -11,6 +11,39 @@ module Metrics def self.tags_for_team(team_name) [Tag.for('team', team_name || UNKNOWN_OWNER)] end + + sig { params(package: ParsePackwerk::Package, app_name: String).returns(T::Array[Tag]) } + def self.tags_for_package(package, app_name) + [ + Tag.new(key: 'package', value: humanized_package_name(package.name)), + Tag.new(key: 'app', value: app_name), + *Metrics.tags_for_team(CodeOwnership.for_package(package)&.name), + ] + end + + sig { params(team_name: T.nilable(String)).returns(T::Array[Tag]) } + def self.tags_for_to_team(team_name) + [Tag.for('to_team', team_name || Metrics::UNKNOWN_OWNER)] + end + + sig { params(name: String).returns(String) } + def self.humanized_package_name(name) + if name == ParsePackwerk::ROOT_PACKAGE_NAME + 'root' + else + name + end + end + + sig { params(violations: T::Array[ParsePackwerk::Violation]).returns(Integer) } + def self.file_count(violations) + violations.sum { |v| v.files.count } + end + + sig { params(package: ParsePackwerk::Package).returns(T::Boolean) } + def self.has_readme?(package) + package.directory.join('README.md').exist? + end end end end diff --git a/lib/modularization_statistics/private/metrics/files.rb b/lib/modularization_statistics/private/metrics/files.rb index bae33bd..a55c8cb 100644 --- a/lib/modularization_statistics/private/metrics/files.rb +++ b/lib/modularization_statistics/private/metrics/files.rb @@ -1,16 +1,12 @@ # typed: strict # frozen_string_literal: true -require 'dogapi' - module ModularizationStatistics module Private module Metrics class Files extend T::Sig - UNKNOWN_OWNER = T.let('Unknown', String) - sig do params( source_code_files: T::Array[SourceCodeFile], diff --git a/lib/modularization_statistics/private/metrics/packages.rb b/lib/modularization_statistics/private/metrics/packages.rb new file mode 100644 index 0000000..64e80cb --- /dev/null +++ b/lib/modularization_statistics/private/metrics/packages.rb @@ -0,0 +1,109 @@ +# typed: strict +# frozen_string_literal: true + +module ModularizationStatistics + module Private + module Metrics + class Packages + extend T::Sig + + sig do + params( + packages: T::Array[ParsePackwerk::Package], + app_name: String + ).returns(T::Array[GaugeMetric]) + end + def self.get_package_metrics(packages, app_name) + all_metrics = [] + app_level_tag = Tag.for('app', app_name) + package_tags = T.let([app_level_tag], T::Array[Tag]) + protected_packages = packages.map { |p| PackageProtections::ProtectedPackage.from(p) } + + all_metrics << GaugeMetric.for('all_packages.count', packages.count, package_tags) + all_metrics << GaugeMetric.for('all_packages.dependencies.count', packages.sum { |package| package.dependencies.count }, package_tags) + all_metrics << GaugeMetric.for('all_packages.dependency_violations.count', protected_packages.sum { |package| Metrics.file_count(package.violations.select(&:dependency?)) }, package_tags) + all_metrics << GaugeMetric.for('all_packages.privacy_violations.count', protected_packages.sum { |package| Metrics.file_count(package.violations.select(&:privacy?)) }, package_tags) + all_metrics << GaugeMetric.for('all_packages.enforcing_dependencies.count', packages.count(&:enforces_dependencies?), package_tags) + all_metrics << GaugeMetric.for('all_packages.enforcing_privacy.count', packages.count(&:enforces_privacy?), package_tags) + + all_metrics << GaugeMetric.for('all_packages.notify_on_package_yml_changes.count', packages.count { |p| p.metadata['notify_on_package_yml_changes'] }, package_tags) + all_metrics << GaugeMetric.for('all_packages.notify_on_new_violations.count', packages.count { |p| p.metadata['notify_on_new_violations'] }, package_tags) + + all_metrics << GaugeMetric.for('all_packages.with_violations.count', protected_packages.count { |package| package.violations.any? }, package_tags) + all_metrics += Metrics::PublicUsage.get_public_usage_metrics('all_packages', packages, package_tags) + all_metrics << GaugeMetric.for('all_packages.has_readme.count', packages.count { |package| Metrics.has_readme?(package) }, package_tags) + + all_metrics += Metrics::ProtectionUsage.get_protections_metrics('all_packages', protected_packages, package_tags) + all_metrics << GaugeMetric.for('all_packages.package_based_file_ownership.count', packages.count { |package| !package.metadata['owner'].nil? }, package_tags) + + inbound_violations_by_package = protected_packages.flat_map(&:violations).group_by(&:to_package_name) + + protected_packages.each do |protected_package| + package = protected_package.original_package + package_tags = Metrics.tags_for_package(package, app_name) + + # + # VIOLATIONS (implicit dependencies) + # + outbound_violations = protected_package.violations + inbound_violations = inbound_violations_by_package[package.name] || [] + all_dependency_violations = (outbound_violations + inbound_violations).select(&:dependency?) + all_privacy_violations = (outbound_violations + inbound_violations).select(&:privacy?) + + all_metrics << GaugeMetric.for('by_package.dependency_violations.count', Metrics.file_count(all_dependency_violations), package_tags) + all_metrics << GaugeMetric.for('by_package.privacy_violations.count', Metrics.file_count(all_privacy_violations), package_tags) + + all_metrics << GaugeMetric.for('by_package.outbound_dependency_violations.count', Metrics.file_count(outbound_violations.select(&:dependency?)), package_tags) + all_metrics << GaugeMetric.for('by_package.inbound_dependency_violations.count', Metrics.file_count(inbound_violations.select(&:dependency?)), package_tags) + + all_metrics << GaugeMetric.for('by_package.outbound_privacy_violations.count', Metrics.file_count(outbound_violations.select(&:privacy?)), package_tags) + all_metrics << GaugeMetric.for('by_package.inbound_privacy_violations.count', Metrics.file_count(inbound_violations.select(&:privacy?)), package_tags) + + all_metrics += Metrics::PublicUsage.get_public_usage_metrics('by_package', [package], package_tags) + + protected_package.violations.group_by(&:to_package_name).each do |to_package_name, violations| + to_package = ParsePackwerk.find(to_package_name) + if to_package.nil? + raise StandardError, "Could not find matching package #{to_package_name}" + end + + tags = package_tags + [Tag.for('to_package', Metrics.humanized_package_name(to_package_name))] + Metrics.tags_for_to_team(CodeOwnership.for_package(to_package)&.name) + all_metrics << GaugeMetric.for('by_package.outbound_dependency_violations.per_package.count', Metrics.file_count(violations.select(&:dependency?)), tags) + all_metrics << GaugeMetric.for('by_package.outbound_privacy_violations.per_package.count', Metrics.file_count(violations.select(&:privacy?)), tags) + end + end + + inbound_explicit_dependency_by_package = {} + packages.each do |package| + package.dependencies.each do |explicit_dependency| + inbound_explicit_dependency_by_package[explicit_dependency] ||= [] + inbound_explicit_dependency_by_package[explicit_dependency] << package.name + end + end + + packages.each do |package| # rubocop:disable Style/CombinableLoops + package_tags = Metrics.tags_for_package(package, app_name) + + # + # EXPLICIT DEPENDENCIES + # + package.dependencies.each do |explicit_dependency| + to_package = ParsePackwerk.find(explicit_dependency) + if to_package.nil? + raise StandardError, "Could not find matching package #{explicit_dependency}" + end + + tags = package_tags + [Tag.for('to_package', Metrics.humanized_package_name(explicit_dependency))] + Metrics.tags_for_to_team(CodeOwnership.for_package(to_package)&.name) + all_metrics << GaugeMetric.for('by_package.outbound_explicit_dependencies.per_package.count', 1, tags) + end + + all_metrics << GaugeMetric.for('by_package.outbound_explicit_dependencies.count', package.dependencies.count, package_tags) + all_metrics << GaugeMetric.for('by_package.inbound_explicit_dependencies.count', inbound_explicit_dependency_by_package[package.name]&.count || 0, package_tags) + end + + all_metrics + end + end + end + end +end diff --git a/lib/modularization_statistics/private/metrics/protection_usage.rb b/lib/modularization_statistics/private/metrics/protection_usage.rb new file mode 100644 index 0000000..ad18f11 --- /dev/null +++ b/lib/modularization_statistics/private/metrics/protection_usage.rb @@ -0,0 +1,33 @@ +# typed: strict +# frozen_string_literal: true + +module ModularizationStatistics + module Private + module Metrics + class ProtectionUsage + extend T::Sig + + sig { params(prefix: String, protected_packages: T::Array[PackageProtections::ProtectedPackage], package_tags: T::Array[Tag]).returns(T::Array[GaugeMetric]) } + def self.get_protections_metrics(prefix, protected_packages, package_tags) + PackageProtections.all.flat_map do |protection| + PackageProtections::ViolationBehavior.each_value.map do |violation_behavior| + # https://github.com/Gusto/package_protections/pull/42 changed the public API of these violation behaviors. + # To preserve our ability to understand historical trends, we map to the old values. + # This allows our dashboards to continue to operate as expected. + # Note if we ever open source mod stats, we should probably inject this behavior so that new clients can see the new keys in their metrics. + violation_behavior_map = { + PackageProtections::ViolationBehavior::FailOnAny => 'fail_the_build_on_any_instances', + PackageProtections::ViolationBehavior::FailNever => 'no', + PackageProtections::ViolationBehavior::FailOnNew => 'fail_the_build_if_new_instances_appear', + } + violation_behavior_name = violation_behavior_map[violation_behavior] + metric_name = "#{prefix}.#{protection.identifier}.#{violation_behavior_name}.count" + count_of_packages = protected_packages.count { |p| p.violation_behavior_for(protection.identifier) == violation_behavior } + GaugeMetric.for(metric_name, count_of_packages, package_tags) + end + end + end + end + end + end +end diff --git a/lib/modularization_statistics/private/metrics/public_usage.rb b/lib/modularization_statistics/private/metrics/public_usage.rb new file mode 100644 index 0000000..d2565e6 --- /dev/null +++ b/lib/modularization_statistics/private/metrics/public_usage.rb @@ -0,0 +1,36 @@ +# typed: strict +# frozen_string_literal: true + +module ModularizationStatistics + module Private + module Metrics + class PublicUsage + extend T::Sig + + sig { params(prefix: String, packages: T::Array[ParsePackwerk::Package], package_tags: T::Array[Tag]).returns(T::Array[GaugeMetric]) } + def self.get_public_usage_metrics(prefix, packages, package_tags) + packages_except_for_root = packages.reject { |package| package.name == ParsePackwerk::ROOT_PACKAGE_NAME } + all_files = packages_except_for_root.flat_map do |package| + package.directory.glob('**/**.rb') + end + + all_public_files = T.let([], T::Array[Pathname]) + is_using_public_directory = 0 + packages_except_for_root.each do |package| + public_files = package.directory.glob('app/public/**/**.rb') + all_public_files += public_files + is_using_public_directory += 1 if public_files.any? + end + + # In Datadog, we can divide public files by all files to get the ratio. + # This is not a metric that we are targeting -- its for observability and reflection only. + [ + GaugeMetric.for("#{prefix}.all_files.count", all_files.count, package_tags), + GaugeMetric.for("#{prefix}.public_files.count", all_public_files.count, package_tags), + GaugeMetric.for("#{prefix}.using_public_directory.count", is_using_public_directory, package_tags), + ] + end + end + end + end +end From 11dff1987efc5ef5c0f58ade98fa44c7a047485b Mon Sep 17 00:00:00 2001 From: Alex Evanczuk Date: Thu, 18 Aug 2022 07:46:15 -0400 Subject: [PATCH 3/4] move packages by team into own class --- .../private/datadog_reporter.rb | 64 +--------------- .../private/metrics/packages_by_team.rb | 73 +++++++++++++++++++ 2 files changed, 75 insertions(+), 62 deletions(-) create mode 100644 lib/modularization_statistics/private/metrics/packages_by_team.rb diff --git a/lib/modularization_statistics/private/datadog_reporter.rb b/lib/modularization_statistics/private/datadog_reporter.rb index 3335056..0ca0fcc 100644 --- a/lib/modularization_statistics/private/datadog_reporter.rb +++ b/lib/modularization_statistics/private/datadog_reporter.rb @@ -7,6 +7,7 @@ require 'modularization_statistics/private/metrics/public_usage' require 'modularization_statistics/private/metrics/protection_usage' require 'modularization_statistics/private/metrics/packages' +require 'modularization_statistics/private/metrics/packages_by_team' module ModularizationStatistics module Private @@ -24,7 +25,7 @@ def self.get_metrics(source_code_files:, app_name:) all_metrics += Metrics::Files.get_metrics(source_code_files, app_name) packages = ParsePackwerk.all all_metrics += Metrics::Packages.get_package_metrics(packages, app_name) - all_metrics += get_package_metrics_by_team(packages, app_name) + all_metrics += Metrics::PackagesByTeam.get_package_metrics_by_team(packages, app_name) all_metrics end @@ -52,67 +53,6 @@ def self.report!(datadog_client:, report_time:, metrics:) end end end - - sig do - params( - all_packages: T::Array[ParsePackwerk::Package], - app_name: String - ).returns(T::Array[GaugeMetric]) - end - def self.get_package_metrics_by_team(all_packages, app_name) - all_metrics = T.let([], T::Array[GaugeMetric]) - app_level_tag = Tag.for('app', app_name) - all_protected_packages = all_packages.map { |p| PackageProtections::ProtectedPackage.from(p) } - all_protected_packages.group_by { |protected_package| CodeOwnership.for_package(protected_package.original_package)&.name }.each do |team_name, protected_packages_by_team| - # We look at `all_packages` because we care about ALL inbound violations across all teams - inbound_violations_by_package = all_protected_packages.flat_map(&:violations).group_by(&:to_package_name) - - team_tags = Metrics.tags_for_team(team_name) + [app_level_tag] - all_metrics << GaugeMetric.for('by_team.all_packages.count', protected_packages_by_team.count, team_tags) - all_metrics += Metrics::ProtectionUsage.get_protections_metrics('by_team', protected_packages_by_team, team_tags) - all_metrics += Metrics::PublicUsage.get_public_usage_metrics('by_team', protected_packages_by_team.map(&:original_package), team_tags) - - all_metrics << GaugeMetric.for('by_team.notify_on_package_yml_changes.count', protected_packages_by_team.count { |p| p.metadata['notify_on_package_yml_changes'] }, team_tags) - all_metrics << GaugeMetric.for('by_team.notify_on_new_violations.count', protected_packages_by_team.count { |p| p.metadata['notify_on_new_violations'] }, team_tags) - - # - # VIOLATIONS (implicit dependencies) - # - outbound_violations = protected_packages_by_team.flat_map(&:violations) - # Here we only look at packages_by_team because we only care about inbound violations onto packages for this team - inbound_violations = protected_packages_by_team.flat_map { |package| inbound_violations_by_package[package.name] || [] } - all_dependency_violations = (outbound_violations + inbound_violations).select(&:dependency?) - all_privacy_violations = (outbound_violations + inbound_violations).select(&:privacy?) - - all_metrics << GaugeMetric.for('by_team.dependency_violations.count', Metrics.file_count(all_dependency_violations), team_tags) - all_metrics << GaugeMetric.for('by_team.privacy_violations.count', Metrics.file_count(all_privacy_violations), team_tags) - - all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.count', Metrics.file_count(outbound_violations.select(&:dependency?)), team_tags) - all_metrics << GaugeMetric.for('by_team.inbound_dependency_violations.count', Metrics.file_count(inbound_violations.select(&:dependency?)), team_tags) - - all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.count', Metrics.file_count(outbound_violations.select(&:privacy?)), team_tags) - all_metrics << GaugeMetric.for('by_team.inbound_privacy_violations.count', Metrics.file_count(inbound_violations.select(&:privacy?)), team_tags) - - all_metrics << GaugeMetric.for('by_team.has_readme.count', protected_packages_by_team.count { |protected_package| Metrics.has_readme?(protected_package.original_package) }, team_tags) - - grouped_outbound_violations = outbound_violations.group_by do |violation| - to_package = ParsePackwerk.find(violation.to_package_name) - if to_package.nil? - raise StandardError, "Could not find matching package #{violation.to_package_name}" - end - - CodeOwnership.for_package(to_package)&.name - end - - grouped_outbound_violations.each do |to_team_name, violations| - tags = team_tags + Metrics.tags_for_to_team(to_team_name) - all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.per_team.count', Metrics.file_count(violations.select(&:dependency?)), tags) - all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.per_team.count', Metrics.file_count(violations.select(&:privacy?)), tags) - end - end - - all_metrics - end end end end diff --git a/lib/modularization_statistics/private/metrics/packages_by_team.rb b/lib/modularization_statistics/private/metrics/packages_by_team.rb new file mode 100644 index 0000000..2aefc5a --- /dev/null +++ b/lib/modularization_statistics/private/metrics/packages_by_team.rb @@ -0,0 +1,73 @@ +# typed: strict +# frozen_string_literal: true + +module ModularizationStatistics + module Private + module Metrics + class PackagesByTeam + extend T::Sig + + sig do + params( + all_packages: T::Array[ParsePackwerk::Package], + app_name: String + ).returns(T::Array[GaugeMetric]) + end + def self.get_package_metrics_by_team(all_packages, app_name) + all_metrics = T.let([], T::Array[GaugeMetric]) + app_level_tag = Tag.for('app', app_name) + all_protected_packages = all_packages.map { |p| PackageProtections::ProtectedPackage.from(p) } + all_protected_packages.group_by { |protected_package| CodeOwnership.for_package(protected_package.original_package)&.name }.each do |team_name, protected_packages_by_team| + # We look at `all_packages` because we care about ALL inbound violations across all teams + inbound_violations_by_package = all_protected_packages.flat_map(&:violations).group_by(&:to_package_name) + + team_tags = Metrics.tags_for_team(team_name) + [app_level_tag] + all_metrics << GaugeMetric.for('by_team.all_packages.count', protected_packages_by_team.count, team_tags) + all_metrics += Metrics::ProtectionUsage.get_protections_metrics('by_team', protected_packages_by_team, team_tags) + all_metrics += Metrics::PublicUsage.get_public_usage_metrics('by_team', protected_packages_by_team.map(&:original_package), team_tags) + + all_metrics << GaugeMetric.for('by_team.notify_on_package_yml_changes.count', protected_packages_by_team.count { |p| p.metadata['notify_on_package_yml_changes'] }, team_tags) + all_metrics << GaugeMetric.for('by_team.notify_on_new_violations.count', protected_packages_by_team.count { |p| p.metadata['notify_on_new_violations'] }, team_tags) + + # + # VIOLATIONS (implicit dependencies) + # + outbound_violations = protected_packages_by_team.flat_map(&:violations) + # Here we only look at packages_by_team because we only care about inbound violations onto packages for this team + inbound_violations = protected_packages_by_team.flat_map { |package| inbound_violations_by_package[package.name] || [] } + all_dependency_violations = (outbound_violations + inbound_violations).select(&:dependency?) + all_privacy_violations = (outbound_violations + inbound_violations).select(&:privacy?) + + all_metrics << GaugeMetric.for('by_team.dependency_violations.count', Metrics.file_count(all_dependency_violations), team_tags) + all_metrics << GaugeMetric.for('by_team.privacy_violations.count', Metrics.file_count(all_privacy_violations), team_tags) + + all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.count', Metrics.file_count(outbound_violations.select(&:dependency?)), team_tags) + all_metrics << GaugeMetric.for('by_team.inbound_dependency_violations.count', Metrics.file_count(inbound_violations.select(&:dependency?)), team_tags) + + all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.count', Metrics.file_count(outbound_violations.select(&:privacy?)), team_tags) + all_metrics << GaugeMetric.for('by_team.inbound_privacy_violations.count', Metrics.file_count(inbound_violations.select(&:privacy?)), team_tags) + + all_metrics << GaugeMetric.for('by_team.has_readme.count', protected_packages_by_team.count { |protected_package| Metrics.has_readme?(protected_package.original_package) }, team_tags) + + grouped_outbound_violations = outbound_violations.group_by do |violation| + to_package = ParsePackwerk.find(violation.to_package_name) + if to_package.nil? + raise StandardError, "Could not find matching package #{violation.to_package_name}" + end + + CodeOwnership.for_package(to_package)&.name + end + + grouped_outbound_violations.each do |to_team_name, violations| + tags = team_tags + Metrics.tags_for_to_team(to_team_name) + all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.per_team.count', Metrics.file_count(violations.select(&:dependency?)), tags) + all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.per_team.count', Metrics.file_count(violations.select(&:privacy?)), tags) + end + end + + all_metrics + end + end + end + end +end From 6d396cea453ae93da9e6dec7fb2598b07a2d22e1 Mon Sep 17 00:00:00 2001 From: Alex Evanczuk Date: Thu, 18 Aug 2022 07:47:03 -0400 Subject: [PATCH 4/4] simplify calling of metrics gatherers --- .../private/datadog_reporter.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/modularization_statistics/private/datadog_reporter.rb b/lib/modularization_statistics/private/datadog_reporter.rb index 0ca0fcc..20943ef 100644 --- a/lib/modularization_statistics/private/datadog_reporter.rb +++ b/lib/modularization_statistics/private/datadog_reporter.rb @@ -21,13 +21,13 @@ class DatadogReporter ).returns(T::Array[GaugeMetric]) end def self.get_metrics(source_code_files:, app_name:) - all_metrics = T.let([], T::Array[GaugeMetric]) - all_metrics += Metrics::Files.get_metrics(source_code_files, app_name) packages = ParsePackwerk.all - all_metrics += Metrics::Packages.get_package_metrics(packages, app_name) - all_metrics += Metrics::PackagesByTeam.get_package_metrics_by_team(packages, app_name) - all_metrics + [ + *Metrics::Files.get_metrics(source_code_files, app_name), + *Metrics::Packages.get_package_metrics(packages, app_name), + *Metrics::PackagesByTeam.get_package_metrics_by_team(packages, app_name) + ] end sig do