pack_stats 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +127 -0
- data/lib/pack_stats/gauge_metric.rb +39 -0
- data/lib/pack_stats/private/datadog_reporter.rb +61 -0
- data/lib/pack_stats/private/metrics/files.rb +47 -0
- data/lib/pack_stats/private/metrics/nested_packs.rb +137 -0
- data/lib/pack_stats/private/metrics/packages.rb +105 -0
- data/lib/pack_stats/private/metrics/packages_by_team.rb +69 -0
- data/lib/pack_stats/private/metrics/packwerk_checker_usage.rb +52 -0
- data/lib/pack_stats/private/metrics/public_usage.rb +36 -0
- data/lib/pack_stats/private/metrics/rubocop_usage.rb +98 -0
- data/lib/pack_stats/private/metrics.rb +49 -0
- data/lib/pack_stats/private/source_code_file.rb +24 -0
- data/lib/pack_stats/private.rb +54 -0
- data/lib/pack_stats/tag.rb +28 -0
- data/lib/pack_stats/tags.rb +20 -0
- data/lib/pack_stats.rb +154 -0
- data/sorbet/config +4 -0
- data/sorbet/rbi/gems/code_ownership@1.28.0.rbi +337 -0
- data/sorbet/rbi/gems/code_teams@1.0.0.rbi +120 -0
- data/sorbet/rbi/gems/dogapi@1.45.0.rbi +551 -0
- data/sorbet/rbi/gems/manual.rbi +4 -0
- data/sorbet/rbi/gems/parse_packwerk@0.14.0.rbi +155 -0
- data/sorbet/rbi/gems/rspec@3.10.0.rbi +13 -0
- data/sorbet/rbi/gems/rubocop-packs@0.0.20.rbi +141 -0
- data/sorbet/rbi/todo.rbi +5 -0
- metadata +228 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 60606e6b527c76b27deaac01ad5991ebe06ec7db76bc5fe9eb707d0cfdde9c56
|
4
|
+
data.tar.gz: ec768521fe361499a52bb29b59b97d23249ef9bd6a9c1bc3c543c8d46d6ccc08
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f33cff3e350f90dbd71cf07433df45bfc170fd9c2101e5713cae1976e034385af98fbc76eddc01ac0ed78009f6e23f2b2f43797999d1fd6e8405fbdb50c013b1
|
7
|
+
data.tar.gz: f04b2b9ac1e3cdf3fcdf0811a1d2a9d1b0a8b2864278ed892caf61c1676fcf762839d24f03ef0f7625895ec37287dcaf3cc034c7ab672468d9b5b1ccf18a15a6
|
data/README.md
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
# PackStats
|
2
|
+
|
3
|
+
This gem is used to report opinionated statistics about modularization to DataDog and other observability systems.
|
4
|
+
|
5
|
+
# Configuring Ownership
|
6
|
+
The gem reports metrics per-team, where each team is configured based on metadata included in Packwerk package.yml files.
|
7
|
+
|
8
|
+
Define your teams as described in the [Code Team - Package Based Ownership](https://github.com/rubyatscale/code_ownership#package-based-ownership) documentation.
|
9
|
+
|
10
|
+
# Usage
|
11
|
+
The main method to this gem is `PackStats#report_to_datadog!`. Refer to the Sorbet signature for this method for the exact types to be passed in.
|
12
|
+
|
13
|
+
This is an example of how to use this API:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
PackStats.report_to_datadog!(
|
17
|
+
#
|
18
|
+
# A properly initialized `Dogapi::Client`
|
19
|
+
# Example: Dogapi::Client.new(ENV.fetch('DATADOG_API_KEY')
|
20
|
+
#
|
21
|
+
datadog_client: datadog_client,
|
22
|
+
#
|
23
|
+
# Time attached to the metrics
|
24
|
+
# Example: Time.now
|
25
|
+
#
|
26
|
+
report_time: report_time
|
27
|
+
#
|
28
|
+
# This is used to determine what files to look at for building statistics about what types of files are packaged, componentized, or unpackaged.
|
29
|
+
# This is an array of `Pathname`. `Pathname` can be relative or absolute paths.
|
30
|
+
#
|
31
|
+
# Example: source_code_pathnames = Pathname.glob('./**/**.rb')
|
32
|
+
#
|
33
|
+
source_code_pathnames: source_code_pathnames,
|
34
|
+
#
|
35
|
+
# A file is determined to be componentized if it exists in any of these directories.
|
36
|
+
# This is an array of `Pathname`. `Pathname` can be relative or absolute paths.
|
37
|
+
#
|
38
|
+
# Example: [Pathname.new("./gems")]
|
39
|
+
#
|
40
|
+
componentized_source_code_locations: componentized_source_code_locations,
|
41
|
+
#
|
42
|
+
# A file is determined to be packaged if it exists in any of these directories.
|
43
|
+
# This is an array of `Pathname`. `Pathname` can be relative or absolute paths.
|
44
|
+
#
|
45
|
+
# Example: [Pathname.new("./packs")]
|
46
|
+
#
|
47
|
+
packaged_source_code_locations: packaged_source_code_locations,
|
48
|
+
)
|
49
|
+
```
|
50
|
+
|
51
|
+
It's recommended to run this in CI on the main/development branch so each new commit has metrics emitted for it.
|
52
|
+
|
53
|
+
# Tracking Privacy and Dependency Violations Reliably
|
54
|
+
With [`packwerk`](https://github.com/Shopify/packwerk), privacy and dependency violations do not show up until a package has set `enforce_privacy` and `enforce_dependency` (respectively) to `true`. As such, when you're first starting off, you'll see no violations, and then periodic large increases as teams start using these protections. If you're interested in looking at privacy and dependency violations over time as if all packages were enforcing dependencies and privacy the whole time, we recommend setting these values to be true before running modularization statistics in your CI.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
require 'pack_stats'
|
58
|
+
|
59
|
+
namespace(:modularization) do
|
60
|
+
desc(
|
61
|
+
'Publish modularization stats to datadog. ' \
|
62
|
+
'Example: bin/rails "modularization:upload_statistics"'
|
63
|
+
)
|
64
|
+
task(:upload_statistics, [:verbose] => :environment) do |_, args|
|
65
|
+
ignored_paths = Pathname.glob('spec/fixtures/**/**')
|
66
|
+
source_code_pathnames = Pathname.glob('{app,components,lib,packs,spec}/**/**').select(&:file?) - ignored_paths
|
67
|
+
|
68
|
+
# To correctly track violations, we rewrite all `package.yml` files with
|
69
|
+
# `enforce_dependencies` and `enforce_privacy` set to true, then update deprecations.
|
70
|
+
old_packages = ParsePackwerk.all
|
71
|
+
old_packages.each do |package|
|
72
|
+
new_package = ParsePackwerk::Package.new(
|
73
|
+
dependencies: package.dependencies,
|
74
|
+
enforce_dependencies: true,
|
75
|
+
enforce_privacy: true,
|
76
|
+
metadata: package.metadata,
|
77
|
+
name: package.name
|
78
|
+
)
|
79
|
+
ParsePackwerk.write_package_yml!(new_package)
|
80
|
+
end
|
81
|
+
|
82
|
+
Packwerk::Cli.new.execute_command(['update-deprecations'])
|
83
|
+
|
84
|
+
# Now we reset it back so that the protection values are the same as the native packwerk configuration
|
85
|
+
old_packages.each do |package|
|
86
|
+
new_package = ParsePackwerk::Package.new(
|
87
|
+
dependencies: package.dependencies,
|
88
|
+
enforce_dependencies: package.enforce_dependencies,
|
89
|
+
enforce_privacy: package.enforce_privacy,
|
90
|
+
metadata: package.metadata,
|
91
|
+
name: package.name
|
92
|
+
)
|
93
|
+
ParsePackwerk.write_package_yml!(new_package)
|
94
|
+
end
|
95
|
+
|
96
|
+
PackStats.report_to_datadog!(
|
97
|
+
datadog_client: Dogapi::Client.new(ENV.fetch('DATADOG_API_KEY')),
|
98
|
+
app_name: Rails.application.class.module_parent_name,
|
99
|
+
source_code_pathnames: source_code_pathnames,
|
100
|
+
verbose: args[:verbose] == 'true' || false
|
101
|
+
)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
# Using Other Observability Tools
|
107
|
+
|
108
|
+
Right now this tool sends metrics to DataDog early. However, if you want to use this with other tools, you can call `PackStats.get_metrics(...)` to get generic metrics that you can then send to whatever observability provider you use.
|
109
|
+
|
110
|
+
# Setting Up Your Dashboards
|
111
|
+
|
112
|
+
Gusto has two dashboards that we've created to view these metrics. We've also exported and released the Dashboard JSON for each of these dashboards. You can create a new dashboard and then click "import dashboard JSON" to get a jump start on tracking your metrics. Note you may want to make some tweaks to these dashboards to better fit your organization's circumstances and goals.
|
113
|
+
|
114
|
+
## [Modularization] Executive Summary
|
115
|
+
|
116
|
+
This helps answer questions like:
|
117
|
+
- How are we doing on reducing dependency and privacy violations in your monolith overall?
|
118
|
+
- How are we doing overall on adopting package protections?
|
119
|
+
|
120
|
+
[Dashboard JSON](docs/executive_summary.json)
|
121
|
+
|
122
|
+
## [Modularization] Per-Package and Per-Team
|
123
|
+
- How is each team and package doing on reducing dependency and privacy violations in your monolith?
|
124
|
+
- What is the total count of dependency/privacy violations for each pack/team and what's the change since last month?
|
125
|
+
- Which pack/team does my pack/team have the most dependency/privacy violations on?
|
126
|
+
|
127
|
+
[Dashboard JSON](docs/per_package_and_per_team.json)
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module PackStats
|
4
|
+
class GaugeMetric < T::Struct
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
const :name, String
|
8
|
+
const :count, Integer
|
9
|
+
const :tags, T::Array[Tag]
|
10
|
+
|
11
|
+
sig { params(metric_name: String, count: Integer, tags: T::Array[Tag]).returns(GaugeMetric) }
|
12
|
+
def self.for(metric_name, count, tags)
|
13
|
+
name = "modularization.#{metric_name}"
|
14
|
+
# https://docs.datadoghq.com/metrics/custom_metrics/#naming-custom-metrics
|
15
|
+
# Metric names must not exceed 200 characters. Fewer than 100 is preferred from a UI perspective
|
16
|
+
if name.length > 200
|
17
|
+
raise StandardError.new("Metrics names must not exceed 200 characters: #{name}") # rubocop:disable Style/RaiseArgs
|
18
|
+
end
|
19
|
+
|
20
|
+
new(
|
21
|
+
name: name,
|
22
|
+
count: count,
|
23
|
+
tags: tags
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
sig { returns(String) }
|
28
|
+
def to_s
|
29
|
+
"#{name} with count #{count}, with tags #{tags.map(&:to_s).join(', ')}"
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { params(other: GaugeMetric).returns(T::Boolean) }
|
33
|
+
def ==(other)
|
34
|
+
other.name == self.name &&
|
35
|
+
other.count == self.count &&
|
36
|
+
other.tags == self.tags
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'dogapi'
|
5
|
+
require 'pack_stats/private/metrics'
|
6
|
+
require 'pack_stats/private/metrics/files'
|
7
|
+
require 'pack_stats/private/metrics/public_usage'
|
8
|
+
require 'pack_stats/private/metrics/packwerk_checker_usage'
|
9
|
+
require 'pack_stats/private/metrics/rubocop_usage'
|
10
|
+
require 'pack_stats/private/metrics/packages'
|
11
|
+
require 'pack_stats/private/metrics/packages_by_team'
|
12
|
+
require 'pack_stats/private/metrics/nested_packs'
|
13
|
+
|
14
|
+
module PackStats
|
15
|
+
module Private
|
16
|
+
class DatadogReporter
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
sig do
|
20
|
+
params(
|
21
|
+
source_code_files: T::Array[SourceCodeFile],
|
22
|
+
app_name: String
|
23
|
+
).returns(T::Array[GaugeMetric])
|
24
|
+
end
|
25
|
+
def self.get_metrics(source_code_files:, app_name:)
|
26
|
+
packages = ParsePackwerk.all
|
27
|
+
|
28
|
+
[
|
29
|
+
*Metrics::Files.get_metrics(source_code_files, app_name),
|
30
|
+
*Metrics::Packages.get_package_metrics(packages, app_name),
|
31
|
+
*Metrics::PackagesByTeam.get_package_metrics_by_team(packages, app_name),
|
32
|
+
*Metrics::NestedPacks.get_nested_package_metrics(packages, app_name)
|
33
|
+
]
|
34
|
+
end
|
35
|
+
|
36
|
+
sig do
|
37
|
+
params(
|
38
|
+
datadog_client: Dogapi::Client,
|
39
|
+
# Since `gauge` buckets data points we need to use the same time for all API calls
|
40
|
+
# to ensure they fall into the same bucket.
|
41
|
+
report_time: Time,
|
42
|
+
metrics: T::Array[GaugeMetric]
|
43
|
+
).void
|
44
|
+
end
|
45
|
+
def self.report!(datadog_client:, report_time:, metrics:)
|
46
|
+
#
|
47
|
+
# Batching the metrics sends a post request to DD
|
48
|
+
# we want to split this up into chunks of 1000 so that as we add more metrics,
|
49
|
+
# our payload is not rejected for being too large
|
50
|
+
#
|
51
|
+
metrics.each_slice(1000).each do |metric_slice|
|
52
|
+
datadog_client.batch_metrics do
|
53
|
+
metric_slice.each do |metric|
|
54
|
+
datadog_client.emit_points(metric.name, [[report_time, metric.count]], type: 'gauge', tags: metric.tags.map(&:to_s))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module PackStats
|
5
|
+
module Private
|
6
|
+
module Metrics
|
7
|
+
class Files
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig do
|
11
|
+
params(
|
12
|
+
source_code_files: T::Array[SourceCodeFile],
|
13
|
+
app_name: String
|
14
|
+
).returns(T::Array[GaugeMetric])
|
15
|
+
end
|
16
|
+
def self.get_metrics(source_code_files, app_name)
|
17
|
+
all_metrics = T.let([], T::Array[GaugeMetric])
|
18
|
+
app_level_tag = Tag.for('app', app_name)
|
19
|
+
|
20
|
+
source_code_files.group_by { |file| file.team_owner&.name }.each do |team_name, files_for_team|
|
21
|
+
file_tags = Metrics.tags_for_team(team_name) + [app_level_tag]
|
22
|
+
all_metrics += get_file_metrics('by_team', file_tags, files_for_team)
|
23
|
+
end
|
24
|
+
|
25
|
+
file_tags = [app_level_tag]
|
26
|
+
all_metrics += get_file_metrics('totals', file_tags, source_code_files)
|
27
|
+
all_metrics
|
28
|
+
end
|
29
|
+
|
30
|
+
sig do
|
31
|
+
params(
|
32
|
+
metric_name_suffix: String,
|
33
|
+
tags: T::Array[Tag],
|
34
|
+
files: T::Array[SourceCodeFile]
|
35
|
+
).returns(T::Array[GaugeMetric])
|
36
|
+
end
|
37
|
+
def self.get_file_metrics(metric_name_suffix, tags, files)
|
38
|
+
[
|
39
|
+
GaugeMetric.for("component_files.#{metric_name_suffix}", files.count(&:componentized_file?), tags),
|
40
|
+
GaugeMetric.for("packaged_files.#{metric_name_suffix}", files.count(&:packaged_file?), tags),
|
41
|
+
GaugeMetric.for("all_files.#{metric_name_suffix}", files.count, tags),
|
42
|
+
]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module PackStats
|
5
|
+
module Private
|
6
|
+
module Metrics
|
7
|
+
class NestedPacks
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
class PackGroup < T::Struct
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
const :name, String
|
14
|
+
const :root, ParsePackwerk::Package
|
15
|
+
const :members, T::Array[ParsePackwerk::Package]
|
16
|
+
|
17
|
+
sig { params(packages: T::Array[ParsePackwerk::Package]).returns(T::Array[PackGroup]) }
|
18
|
+
def self.all_from(packages)
|
19
|
+
packs_by_group = {}
|
20
|
+
|
21
|
+
packages.each do |package|
|
22
|
+
# For a child pack, package.directory is `packs/fruits/apples` (i.e. the directory of the package.yml file).
|
23
|
+
# The package.directory.dirname is therefore `packs/fruits`.
|
24
|
+
# For a standalone pack, package.directory.dirname is `packs`
|
25
|
+
# A pack with no parent is in a pack group of its own name
|
26
|
+
root = ParsePackwerk.find(package.directory.dirname.to_s) || package
|
27
|
+
# Mark the parent pack and child pack as being in the pack group of the parent
|
28
|
+
packs_by_group[root.name] ||= { root: root, members: [] }
|
29
|
+
packs_by_group[root.name][:members] << package
|
30
|
+
end
|
31
|
+
|
32
|
+
packs_by_group.map do |name, pack_data|
|
33
|
+
PackGroup.new(
|
34
|
+
name: name,
|
35
|
+
root: pack_data[:root],
|
36
|
+
members: pack_data[:members],
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
sig { returns(Integer) }
|
42
|
+
def children_pack_count
|
43
|
+
members.count do |package|
|
44
|
+
package.name != root.name
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { returns(T::Boolean) }
|
49
|
+
def has_parent?
|
50
|
+
children_pack_count > 0
|
51
|
+
end
|
52
|
+
|
53
|
+
sig { returns(T::Array[ParsePackwerk::Violation]) }
|
54
|
+
def cross_group_violations
|
55
|
+
all_violations = members.flat_map do |member|
|
56
|
+
ParsePackwerk::DeprecatedReferences.for(member).violations
|
57
|
+
end
|
58
|
+
|
59
|
+
all_violations.select do |violation|
|
60
|
+
!members.map(&:name).include?(violation.to_package_name)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
sig do
|
66
|
+
params(
|
67
|
+
packages: T::Array[ParsePackwerk::Package],
|
68
|
+
app_name: String
|
69
|
+
).returns(T::Array[GaugeMetric])
|
70
|
+
end
|
71
|
+
def self.get_nested_package_metrics(packages, app_name)
|
72
|
+
all_metrics = []
|
73
|
+
app_level_tag = Tag.for('app', app_name)
|
74
|
+
package_tags = T.let([app_level_tag], T::Array[Tag])
|
75
|
+
|
76
|
+
pack_groups = PackGroup.all_from(packages)
|
77
|
+
all_pack_groups_count = pack_groups.count
|
78
|
+
child_pack_count = pack_groups.sum(&:children_pack_count)
|
79
|
+
parent_pack_count = pack_groups.count(&:has_parent?)
|
80
|
+
all_cross_pack_group_violations = pack_groups.flat_map(&:cross_group_violations)
|
81
|
+
|
82
|
+
all_metrics << GaugeMetric.for('all_pack_groups.count', all_pack_groups_count, package_tags)
|
83
|
+
all_metrics << GaugeMetric.for('child_packs.count', child_pack_count, package_tags)
|
84
|
+
all_metrics << GaugeMetric.for('parent_packs.count', parent_pack_count, package_tags)
|
85
|
+
all_metrics << GaugeMetric.for('all_pack_groups.privacy_violations.count', Metrics.file_count(all_cross_pack_group_violations.select(&:privacy?)), package_tags)
|
86
|
+
all_metrics << GaugeMetric.for('all_pack_groups.dependency_violations.count', Metrics.file_count(all_cross_pack_group_violations.select(&:dependency?)), package_tags)\
|
87
|
+
|
88
|
+
packs_by_group = {}
|
89
|
+
pack_groups.each do |pack_group|
|
90
|
+
pack_group.members.each do |member|
|
91
|
+
packs_by_group[member.name] = pack_group.name
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
inbound_violations_by_pack_group = {}
|
96
|
+
all_cross_pack_group_violations.group_by(&:to_package_name).each do |to_package_name, violations|
|
97
|
+
violations.each do |violation|
|
98
|
+
pack_group_for_violation = packs_by_group[violation.to_package_name]
|
99
|
+
inbound_violations_by_pack_group[pack_group_for_violation] ||= []
|
100
|
+
inbound_violations_by_pack_group[pack_group_for_violation] << violation
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
pack_groups.each do |pack_group|
|
105
|
+
tags = [
|
106
|
+
*package_tags,
|
107
|
+
Tag.for('pack_group', Metrics.humanized_package_name(pack_group.name)),
|
108
|
+
]
|
109
|
+
|
110
|
+
outbound_dependency_violations = pack_group.cross_group_violations.select(&:dependency?)
|
111
|
+
inbound_privacy_violations = inbound_violations_by_pack_group.fetch(pack_group.name, []).select(&:privacy?)
|
112
|
+
all_metrics << GaugeMetric.for('by_pack_group.outbound_dependency_violations.count', Metrics.file_count(outbound_dependency_violations), tags)
|
113
|
+
all_metrics << GaugeMetric.for('by_pack_group.inbound_privacy_violations.count', Metrics.file_count(inbound_privacy_violations), tags)
|
114
|
+
end
|
115
|
+
|
116
|
+
pack_groups.each do |from_pack_group|
|
117
|
+
violations_by_to_pack_group = from_pack_group.cross_group_violations.group_by do |violation|
|
118
|
+
packs_by_group[violation.to_package_name]
|
119
|
+
end
|
120
|
+
violations_by_to_pack_group.each do |to_pack_group_name, violations|
|
121
|
+
tags = [
|
122
|
+
*package_tags,
|
123
|
+
Tag.for('pack_group', Metrics.humanized_package_name(from_pack_group.name)),
|
124
|
+
Tag.for('to_pack_group', Metrics.humanized_package_name(to_pack_group_name)),
|
125
|
+
]
|
126
|
+
|
127
|
+
all_metrics << GaugeMetric.for('by_pack_group.outbound_dependency_violations.per_pack_group.count', Metrics.file_count(violations.select(&:dependency?)), tags)
|
128
|
+
all_metrics << GaugeMetric.for('by_pack_group.outbound_privacy_violations.per_pack_group.count', Metrics.file_count(violations.select(&:privacy?)), tags)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
all_metrics
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module PackStats
|
5
|
+
module Private
|
6
|
+
module Metrics
|
7
|
+
class Packages
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig do
|
11
|
+
params(
|
12
|
+
packages: T::Array[ParsePackwerk::Package],
|
13
|
+
app_name: String
|
14
|
+
).returns(T::Array[GaugeMetric])
|
15
|
+
end
|
16
|
+
def self.get_package_metrics(packages, app_name)
|
17
|
+
all_metrics = []
|
18
|
+
app_level_tag = Tag.for('app', app_name)
|
19
|
+
package_tags = T.let([app_level_tag], T::Array[Tag])
|
20
|
+
|
21
|
+
all_metrics << GaugeMetric.for('all_packages.count', packages.count, package_tags)
|
22
|
+
all_metrics << GaugeMetric.for('all_packages.dependencies.count', packages.sum { |package| package.dependencies.count }, package_tags)
|
23
|
+
all_metrics << GaugeMetric.for('all_packages.dependency_violations.count', packages.sum { |package| Metrics.file_count(package.violations.select(&:dependency?)) }, package_tags)
|
24
|
+
all_metrics << GaugeMetric.for('all_packages.privacy_violations.count', packages.sum { |package| Metrics.file_count(package.violations.select(&:privacy?)) }, package_tags)
|
25
|
+
all_metrics << GaugeMetric.for('all_packages.enforcing_dependencies.count', packages.count(&:enforces_dependencies?), package_tags)
|
26
|
+
all_metrics << GaugeMetric.for('all_packages.enforcing_privacy.count', packages.count(&:enforces_privacy?), package_tags)
|
27
|
+
|
28
|
+
all_metrics << GaugeMetric.for('all_packages.with_violations.count', packages.count { |package| package.violations.any? }, package_tags)
|
29
|
+
all_metrics += Metrics::PublicUsage.get_public_usage_metrics('all_packages', packages, package_tags)
|
30
|
+
all_metrics << GaugeMetric.for('all_packages.has_readme.count', packages.count { |package| Metrics.has_readme?(package) }, package_tags)
|
31
|
+
|
32
|
+
all_metrics += Metrics::PackwerkCheckerUsage.get_checker_metrics('all_packages', packages, package_tags)
|
33
|
+
all_metrics += Metrics::RubocopUsage.get_metrics('all_packages', packages, package_tags)
|
34
|
+
all_metrics << GaugeMetric.for('all_packages.package_based_file_ownership.count', packages.count { |package| !package.metadata['owner'].nil? }, package_tags)
|
35
|
+
|
36
|
+
inbound_violations_by_package = packages.flat_map(&:violations).group_by(&:to_package_name)
|
37
|
+
|
38
|
+
packages.each do |package|
|
39
|
+
package_tags = Metrics.tags_for_package(package, app_name)
|
40
|
+
|
41
|
+
#
|
42
|
+
# VIOLATIONS (implicit dependencies)
|
43
|
+
#
|
44
|
+
outbound_violations = package.violations
|
45
|
+
inbound_violations = inbound_violations_by_package[package.name] || []
|
46
|
+
all_dependency_violations = (outbound_violations + inbound_violations).select(&:dependency?)
|
47
|
+
all_privacy_violations = (outbound_violations + inbound_violations).select(&:privacy?)
|
48
|
+
|
49
|
+
all_metrics << GaugeMetric.for('by_package.dependency_violations.count', Metrics.file_count(all_dependency_violations), package_tags)
|
50
|
+
all_metrics << GaugeMetric.for('by_package.privacy_violations.count', Metrics.file_count(all_privacy_violations), package_tags)
|
51
|
+
|
52
|
+
all_metrics << GaugeMetric.for('by_package.outbound_dependency_violations.count', Metrics.file_count(outbound_violations.select(&:dependency?)), package_tags)
|
53
|
+
all_metrics << GaugeMetric.for('by_package.inbound_dependency_violations.count', Metrics.file_count(inbound_violations.select(&:dependency?)), package_tags)
|
54
|
+
|
55
|
+
all_metrics << GaugeMetric.for('by_package.outbound_privacy_violations.count', Metrics.file_count(outbound_violations.select(&:privacy?)), package_tags)
|
56
|
+
all_metrics << GaugeMetric.for('by_package.inbound_privacy_violations.count', Metrics.file_count(inbound_violations.select(&:privacy?)), package_tags)
|
57
|
+
|
58
|
+
all_metrics += Metrics::PublicUsage.get_public_usage_metrics('by_package', [package], package_tags)
|
59
|
+
|
60
|
+
package.violations.group_by(&:to_package_name).each do |to_package_name, violations|
|
61
|
+
to_package = ParsePackwerk.find(to_package_name)
|
62
|
+
if to_package.nil?
|
63
|
+
raise StandardError, "Could not find matching package #{to_package_name}"
|
64
|
+
end
|
65
|
+
|
66
|
+
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)
|
67
|
+
all_metrics << GaugeMetric.for('by_package.outbound_dependency_violations.per_package.count', Metrics.file_count(violations.select(&:dependency?)), tags)
|
68
|
+
all_metrics << GaugeMetric.for('by_package.outbound_privacy_violations.per_package.count', Metrics.file_count(violations.select(&:privacy?)), tags)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
inbound_explicit_dependency_by_package = {}
|
73
|
+
packages.each do |package|
|
74
|
+
package.dependencies.each do |explicit_dependency|
|
75
|
+
inbound_explicit_dependency_by_package[explicit_dependency] ||= []
|
76
|
+
inbound_explicit_dependency_by_package[explicit_dependency] << package.name
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
packages.each do |package| # rubocop:disable Style/CombinableLoops
|
81
|
+
package_tags = Metrics.tags_for_package(package, app_name)
|
82
|
+
|
83
|
+
#
|
84
|
+
# EXPLICIT DEPENDENCIES
|
85
|
+
#
|
86
|
+
package.dependencies.each do |explicit_dependency|
|
87
|
+
to_package = ParsePackwerk.find(explicit_dependency)
|
88
|
+
if to_package.nil?
|
89
|
+
raise StandardError, "Could not find matching package #{explicit_dependency}"
|
90
|
+
end
|
91
|
+
|
92
|
+
tags = package_tags + [Tag.for('to_package', Metrics.humanized_package_name(explicit_dependency))] + Metrics.tags_for_to_team(CodeOwnership.for_package(to_package)&.name)
|
93
|
+
all_metrics << GaugeMetric.for('by_package.outbound_explicit_dependencies.per_package.count', 1, tags)
|
94
|
+
end
|
95
|
+
|
96
|
+
all_metrics << GaugeMetric.for('by_package.outbound_explicit_dependencies.count', package.dependencies.count, package_tags)
|
97
|
+
all_metrics << GaugeMetric.for('by_package.inbound_explicit_dependencies.count', inbound_explicit_dependency_by_package[package.name]&.count || 0, package_tags)
|
98
|
+
end
|
99
|
+
|
100
|
+
all_metrics
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module PackStats
|
5
|
+
module Private
|
6
|
+
module Metrics
|
7
|
+
class PackagesByTeam
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig do
|
11
|
+
params(
|
12
|
+
all_packages: T::Array[ParsePackwerk::Package],
|
13
|
+
app_name: String
|
14
|
+
).returns(T::Array[GaugeMetric])
|
15
|
+
end
|
16
|
+
def self.get_package_metrics_by_team(all_packages, app_name)
|
17
|
+
all_metrics = T.let([], T::Array[GaugeMetric])
|
18
|
+
app_level_tag = Tag.for('app', app_name)
|
19
|
+
|
20
|
+
all_packages.group_by { |package| CodeOwnership.for_package(package)&.name }.each do |team_name, packages_by_team|
|
21
|
+
# We look at `all_packages` because we care about ALL inbound violations across all teams
|
22
|
+
inbound_violations_by_package = all_packages.flat_map(&:violations).group_by(&:to_package_name)
|
23
|
+
|
24
|
+
team_tags = Metrics.tags_for_team(team_name) + [app_level_tag]
|
25
|
+
all_metrics << GaugeMetric.for('by_team.all_packages.count', packages_by_team.count, team_tags)
|
26
|
+
all_metrics += Metrics::PackwerkCheckerUsage.get_checker_metrics('by_team', packages_by_team, team_tags)
|
27
|
+
all_metrics += Metrics::PublicUsage.get_public_usage_metrics('by_team', packages_by_team, team_tags)
|
28
|
+
#
|
29
|
+
# VIOLATIONS (implicit dependencies)
|
30
|
+
#
|
31
|
+
outbound_violations = packages_by_team.flat_map(&:violations)
|
32
|
+
# Here we only look at packages_by_team because we only care about inbound violations onto packages for this team
|
33
|
+
inbound_violations = packages_by_team.flat_map { |package| inbound_violations_by_package[package.name] || [] }
|
34
|
+
all_dependency_violations = (outbound_violations + inbound_violations).select(&:dependency?)
|
35
|
+
all_privacy_violations = (outbound_violations + inbound_violations).select(&:privacy?)
|
36
|
+
|
37
|
+
all_metrics << GaugeMetric.for('by_team.dependency_violations.count', Metrics.file_count(all_dependency_violations), team_tags)
|
38
|
+
all_metrics << GaugeMetric.for('by_team.privacy_violations.count', Metrics.file_count(all_privacy_violations), team_tags)
|
39
|
+
|
40
|
+
all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.count', Metrics.file_count(outbound_violations.select(&:dependency?)), team_tags)
|
41
|
+
all_metrics << GaugeMetric.for('by_team.inbound_dependency_violations.count', Metrics.file_count(inbound_violations.select(&:dependency?)), team_tags)
|
42
|
+
|
43
|
+
all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.count', Metrics.file_count(outbound_violations.select(&:privacy?)), team_tags)
|
44
|
+
all_metrics << GaugeMetric.for('by_team.inbound_privacy_violations.count', Metrics.file_count(inbound_violations.select(&:privacy?)), team_tags)
|
45
|
+
|
46
|
+
all_metrics << GaugeMetric.for('by_team.has_readme.count', packages_by_team.count { |package| Metrics.has_readme?(package) }, team_tags)
|
47
|
+
|
48
|
+
grouped_outbound_violations = outbound_violations.group_by do |violation|
|
49
|
+
to_package = ParsePackwerk.find(violation.to_package_name)
|
50
|
+
if to_package.nil?
|
51
|
+
raise StandardError, "Could not find matching package #{violation.to_package_name}"
|
52
|
+
end
|
53
|
+
|
54
|
+
CodeOwnership.for_package(to_package)&.name
|
55
|
+
end
|
56
|
+
|
57
|
+
grouped_outbound_violations.each do |to_team_name, violations|
|
58
|
+
tags = team_tags + Metrics.tags_for_to_team(to_team_name)
|
59
|
+
all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.per_team.count', Metrics.file_count(violations.select(&:dependency?)), tags)
|
60
|
+
all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.per_team.count', Metrics.file_count(violations.select(&:privacy?)), tags)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
all_metrics
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'rubocop-packs'
|
5
|
+
|
6
|
+
module PackStats
|
7
|
+
module Private
|
8
|
+
module Metrics
|
9
|
+
class PackwerkCheckerUsage
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
# Later, we might find a way we can get this directly from `packwerk`
|
13
|
+
class PackwerkChecker < T::Struct
|
14
|
+
const :setting, String
|
15
|
+
const :strict_mode, String
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { params(prefix: String, packages: T::Array[ParsePackwerk::Package], package_tags: T::Array[Tag]).returns(T::Array[GaugeMetric]) }
|
19
|
+
def self.get_checker_metrics(prefix, packages, package_tags)
|
20
|
+
metrics = T.let([], T::Array[GaugeMetric])
|
21
|
+
|
22
|
+
checkers = [
|
23
|
+
PackwerkChecker.new(setting: 'enforce_dependencies', strict_mode: 'enforce_dependencies_strictly'),
|
24
|
+
PackwerkChecker.new(setting: 'enforce_privacy', strict_mode: 'enforce_privacy_strictly')
|
25
|
+
]
|
26
|
+
|
27
|
+
checkers.each do |checker|
|
28
|
+
['false', 'true', 'strict'].each do |enabled_mode|
|
29
|
+
count_of_packages = ParsePackwerk.all.count do |package|
|
30
|
+
strict_mode = package.metadata[checker.strict_mode]
|
31
|
+
enabled = YAML.load_file(package.yml)[checker.setting]
|
32
|
+
case enabled_mode
|
33
|
+
when 'false'
|
34
|
+
!enabled
|
35
|
+
when 'true'
|
36
|
+
enabled && !strict_mode
|
37
|
+
when 'strict'
|
38
|
+
!!strict_mode
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
metric_name = "#{prefix}.packwerk_checkers.#{checker.setting}.#{enabled_mode}.count"
|
43
|
+
metrics << GaugeMetric.for(metric_name, count_of_packages, package_tags)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
metrics
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|