modularization_statistics 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +67 -0
- data/lib/modularization_statistics/gauge_metric.rb +39 -0
- data/lib/modularization_statistics/private/datadog_reporter.rb +325 -0
- data/lib/modularization_statistics/private/source_code_file.rb +24 -0
- data/lib/modularization_statistics/private.rb +8 -0
- data/lib/modularization_statistics/tag.rb +28 -0
- data/lib/modularization_statistics/tags.rb +20 -0
- data/lib/modularization_statistics.rb +134 -0
- data/sorbet/config +3 -0
- data/sorbet/rbi/gems/bigrails-teams@0.1.0.rbi +120 -0
- data/sorbet/rbi/gems/code_ownership@1.23.0.rbi +336 -0
- data/sorbet/rbi/gems/dogapi@1.45.0.rbi +551 -0
- data/sorbet/rbi/gems/manual.rbi +4 -0
- data/sorbet/rbi/gems/package_protections@0.64.0.rbi +632 -0
- data/sorbet/rbi/gems/parse_packwerk@0.10.0.rbi +140 -0
- data/sorbet/rbi/gems/rspec@3.10.0.rbi +13 -0
- data/sorbet/rbi/todo.rbi +5 -0
- metadata +220 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: dfd014b44b30778ec250a807009b4803b8e4b81c99e4fcc46d70cfca8feca8bb
|
4
|
+
data.tar.gz: ba83025eaa394685d99a28eadb5a7e30893107396b1c723b055e7ab6c6e09895
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ebad79ae91d8d0731d259574f515375ed2306cb6b89458e969141f5c02ff4c88f67f926e7eec2ffc29659c5486d49f61bcbe51fcec5f4157aa277c9ad46b2c7c
|
7
|
+
data.tar.gz: 3b833b67284142468e86361797fec62761db59e9917db398179593bd2df6e4de4bb392a8ce6b999fe91f7bde496aeabba1165b3341e69ba989ac733e2f27de4c
|
data/README.md
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# ModularizationStatistics
|
2
|
+
|
3
|
+
This gem is used to report opinionated statistics about modularization to DataDog and other observability systems.
|
4
|
+
|
5
|
+
# Usage
|
6
|
+
The main method to this gem is `ModularizationStatistics#report_to_datadog!`. Refer to the Sorbet signature for this method for the exact types to be passed in.
|
7
|
+
|
8
|
+
This is an example of how to use this API:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
ModularizationStatistics.report_to_datadog!(
|
12
|
+
#
|
13
|
+
# A properly initialized `Dogapi::Client`
|
14
|
+
# Example: Dogapi::Client.new(ENV.fetch('DATADOG_API_KEY')
|
15
|
+
#
|
16
|
+
datadog_client: datadog_client,
|
17
|
+
#
|
18
|
+
# Time attached to the metrics
|
19
|
+
# Example: Time.now
|
20
|
+
#
|
21
|
+
report_time: report_time
|
22
|
+
#
|
23
|
+
# This is used to determine what files to look at for building statistics about what types of files are packaged, componentized, or unpackaged.
|
24
|
+
# This is an array of `Pathname`. `Pathname` can be relative or absolute paths.
|
25
|
+
#
|
26
|
+
# Example: source_code_pathnames = Pathname.glob('./**/**.rb')
|
27
|
+
#
|
28
|
+
source_code_pathnames: source_code_pathnames,
|
29
|
+
#
|
30
|
+
# A file is determined to be componentized if it exists in any of these directories.
|
31
|
+
# This is an array of `Pathname`. `Pathname` can be relative or absolute paths.
|
32
|
+
#
|
33
|
+
# Example: [Pathname.new("./gems")]
|
34
|
+
#
|
35
|
+
componentized_source_code_locations: componentized_source_code_locations,
|
36
|
+
#
|
37
|
+
# A file is determined to be packaged if it exists in any of these directories.
|
38
|
+
# This is an array of `Pathname`. `Pathname` can be relative or absolute paths.
|
39
|
+
#
|
40
|
+
# Example: [Pathname.new("./packs")]
|
41
|
+
#
|
42
|
+
packaged_source_code_locations: packaged_source_code_locations,
|
43
|
+
)
|
44
|
+
```
|
45
|
+
|
46
|
+
# Using Other Observability Tools
|
47
|
+
|
48
|
+
Right now this tool sends metrics to DataDog early. However, if you want to use this with other tools, you can call `ModularizationStatistics.get_metrics(...)` to get generic metrics that you can then send to whatever observability provider you use.
|
49
|
+
|
50
|
+
# Setting Up Your Dashboards
|
51
|
+
|
52
|
+
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.
|
53
|
+
|
54
|
+
## [Modularization] Executive Summary
|
55
|
+
|
56
|
+
This helps answer questions like:
|
57
|
+
- How are we doing on reducing dependency and privacy violations in your monolith overall?
|
58
|
+
- How are we doing overall on adopting package protections?
|
59
|
+
|
60
|
+
[Dashboard JSON](docs/executive_summary.json)
|
61
|
+
|
62
|
+
## [Modularization] Per-Package and Per-Team
|
63
|
+
- How is each team and package doing on reducing dependency and privacy violations in your monolith?
|
64
|
+
- What is the total count of dependency/privacy violations for each pack/team and what's the change since last month?
|
65
|
+
- Which pack/team does my pack/team have the most dependency/privacy violations on?
|
66
|
+
|
67
|
+
[Dashboard JSON](docs/per_package_and_per_team.json)
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module ModularizationStatistics
|
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,325 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'dogapi'
|
5
|
+
|
6
|
+
module ModularizationStatistics
|
7
|
+
module Private
|
8
|
+
class DatadogReporter
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
UNKNOWN_OWNER = T.let('Unknown', String)
|
12
|
+
|
13
|
+
sig do
|
14
|
+
params(
|
15
|
+
source_code_files: T::Array[SourceCodeFile],
|
16
|
+
app_name: String
|
17
|
+
).returns(T::Array[GaugeMetric])
|
18
|
+
end
|
19
|
+
def self.get_metrics(source_code_files:, app_name:)
|
20
|
+
all_metrics = T.let([], T::Array[GaugeMetric])
|
21
|
+
app_level_tag = Tag.for('app', app_name)
|
22
|
+
|
23
|
+
source_code_files.group_by { |file| file.team_owner&.name }.each do |team_name, files_for_team|
|
24
|
+
file_tags = tags_for_team(team_name) + [app_level_tag]
|
25
|
+
all_metrics += get_file_metrics('by_team', file_tags, files_for_team)
|
26
|
+
end
|
27
|
+
|
28
|
+
file_tags = [app_level_tag]
|
29
|
+
all_metrics += get_file_metrics('totals', file_tags, source_code_files)
|
30
|
+
|
31
|
+
packages = ParsePackwerk.all
|
32
|
+
all_metrics += get_package_metrics(packages, app_name)
|
33
|
+
all_metrics += get_package_metrics_by_team(packages, app_name)
|
34
|
+
|
35
|
+
all_metrics
|
36
|
+
end
|
37
|
+
|
38
|
+
sig do
|
39
|
+
params(
|
40
|
+
datadog_client: Dogapi::Client,
|
41
|
+
# Since `gauge` buckets data points we need to use the same time for all API calls
|
42
|
+
# to ensure they fall into the same bucket.
|
43
|
+
report_time: Time,
|
44
|
+
metrics: T::Array[GaugeMetric]
|
45
|
+
).void
|
46
|
+
end
|
47
|
+
def self.report!(datadog_client:, report_time:, metrics:)
|
48
|
+
#
|
49
|
+
# Batching the metrics sends a post request to DD
|
50
|
+
# we want to split this up into chunks of 1000 so that as we add more metrics,
|
51
|
+
# our payload is not rejected for being too large
|
52
|
+
#
|
53
|
+
metrics.each_slice(1000).each do |metric_slice|
|
54
|
+
datadog_client.batch_metrics do
|
55
|
+
metric_slice.each do |metric|
|
56
|
+
datadog_client.emit_points(metric.name, [[report_time, metric.count]], type: 'gauge', tags: metric.tags.map(&:to_s))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { params(package: ParsePackwerk::Package, app_name: String).returns(T::Array[Tag]) }
|
63
|
+
def self.tags_for_package(package, app_name)
|
64
|
+
[
|
65
|
+
Tag.new(key: 'package', value: humanized_package_name(package.name)),
|
66
|
+
Tag.new(key: 'app', value: app_name),
|
67
|
+
*tags_for_team(CodeOwnership.for_package(package)&.name),
|
68
|
+
]
|
69
|
+
end
|
70
|
+
|
71
|
+
sig { params(team_name: T.nilable(String)).returns(T::Array[Tag]) }
|
72
|
+
def self.tags_for_team(team_name)
|
73
|
+
[Tag.for('team', team_name || UNKNOWN_OWNER)]
|
74
|
+
end
|
75
|
+
|
76
|
+
sig { params(team_name: T.nilable(String)).returns(T::Array[Tag]) }
|
77
|
+
def self.tags_for_to_team(team_name)
|
78
|
+
[Tag.for('to_team', team_name || UNKNOWN_OWNER)]
|
79
|
+
end
|
80
|
+
|
81
|
+
private_class_method :tags_for_package
|
82
|
+
|
83
|
+
sig do
|
84
|
+
params(
|
85
|
+
packages: T::Array[ParsePackwerk::Package],
|
86
|
+
app_name: String
|
87
|
+
).returns(T::Array[GaugeMetric])
|
88
|
+
end
|
89
|
+
def self.get_package_metrics(packages, app_name)
|
90
|
+
all_metrics = []
|
91
|
+
app_level_tag = Tag.for('app', app_name)
|
92
|
+
package_tags = T.let([app_level_tag], T::Array[Tag])
|
93
|
+
protected_packages = packages.map { |p| PackageProtections::ProtectedPackage.from(p) }
|
94
|
+
|
95
|
+
all_metrics << GaugeMetric.for('all_packages.count', packages.count, package_tags)
|
96
|
+
all_metrics << GaugeMetric.for('all_packages.dependencies.count', packages.sum { |package| package.dependencies.count }, package_tags)
|
97
|
+
all_metrics << GaugeMetric.for('all_packages.dependency_violations.count', protected_packages.sum { |package| file_count(package.violations.select(&:dependency?)) }, package_tags)
|
98
|
+
all_metrics << GaugeMetric.for('all_packages.privacy_violations.count', protected_packages.sum { |package| file_count(package.violations.select(&:privacy?)) }, package_tags)
|
99
|
+
all_metrics << GaugeMetric.for('all_packages.enforcing_dependencies.count', packages.count(&:enforces_dependencies?), package_tags)
|
100
|
+
all_metrics << GaugeMetric.for('all_packages.enforcing_privacy.count', packages.count(&:enforces_privacy?), package_tags)
|
101
|
+
|
102
|
+
all_metrics << GaugeMetric.for('all_packages.notify_on_package_yml_changes.count', packages.count { |p| p.metadata['notify_on_package_yml_changes'] }, package_tags)
|
103
|
+
all_metrics << GaugeMetric.for('all_packages.notify_on_new_violations.count', packages.count { |p| p.metadata['notify_on_new_violations'] }, package_tags)
|
104
|
+
|
105
|
+
all_metrics << GaugeMetric.for('all_packages.with_violations.count', protected_packages.count { |package| package.violations.any? }, package_tags)
|
106
|
+
all_metrics += self.get_public_usage_metrics('all_packages', packages, package_tags)
|
107
|
+
all_metrics << GaugeMetric.for('all_packages.has_readme.count', packages.count { |package| has_readme?(package) }, package_tags)
|
108
|
+
|
109
|
+
all_metrics += self.get_protections_metrics('all_packages', protected_packages, package_tags)
|
110
|
+
all_metrics << GaugeMetric.for('all_packages.package_based_file_ownership.count', packages.count { |package| !package.metadata['owner'].nil? }, package_tags)
|
111
|
+
|
112
|
+
inbound_violations_by_package = protected_packages.flat_map(&:violations).group_by(&:to_package_name)
|
113
|
+
|
114
|
+
protected_packages.each do |protected_package|
|
115
|
+
package = protected_package.original_package
|
116
|
+
package_tags = tags_for_package(package, app_name)
|
117
|
+
|
118
|
+
#
|
119
|
+
# VIOLATIONS (implicit dependencies)
|
120
|
+
#
|
121
|
+
outbound_violations = protected_package.violations
|
122
|
+
inbound_violations = inbound_violations_by_package[package.name] || []
|
123
|
+
all_dependency_violations = (outbound_violations + inbound_violations).select(&:dependency?)
|
124
|
+
all_privacy_violations = (outbound_violations + inbound_violations).select(&:privacy?)
|
125
|
+
|
126
|
+
all_metrics << GaugeMetric.for('by_package.dependency_violations.count', file_count(all_dependency_violations), package_tags)
|
127
|
+
all_metrics << GaugeMetric.for('by_package.privacy_violations.count', file_count(all_privacy_violations), package_tags)
|
128
|
+
|
129
|
+
all_metrics << GaugeMetric.for('by_package.outbound_dependency_violations.count', file_count(outbound_violations.select(&:dependency?)), package_tags)
|
130
|
+
all_metrics << GaugeMetric.for('by_package.inbound_dependency_violations.count', file_count(inbound_violations.select(&:dependency?)), package_tags)
|
131
|
+
|
132
|
+
all_metrics << GaugeMetric.for('by_package.outbound_privacy_violations.count', file_count(outbound_violations.select(&:privacy?)), package_tags)
|
133
|
+
all_metrics << GaugeMetric.for('by_package.inbound_privacy_violations.count', file_count(inbound_violations.select(&:privacy?)), package_tags)
|
134
|
+
|
135
|
+
all_metrics += self.get_public_usage_metrics('by_package', [package], package_tags)
|
136
|
+
|
137
|
+
protected_package.violations.group_by(&:to_package_name).each do |to_package_name, violations|
|
138
|
+
to_package = ParsePackwerk.find(to_package_name)
|
139
|
+
if to_package.nil?
|
140
|
+
raise StandardError, "Could not find matching package #{to_package_name}"
|
141
|
+
end
|
142
|
+
|
143
|
+
tags = package_tags + [Tag.for('to_package', humanized_package_name(to_package_name))] + tags_for_to_team(CodeOwnership.for_package(to_package)&.name)
|
144
|
+
all_metrics << GaugeMetric.for('by_package.outbound_dependency_violations.per_package.count', file_count(violations.select(&:dependency?)), tags)
|
145
|
+
all_metrics << GaugeMetric.for('by_package.outbound_privacy_violations.per_package.count', file_count(violations.select(&:privacy?)), tags)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
inbound_explicit_dependency_by_package = {}
|
150
|
+
packages.each do |package|
|
151
|
+
package.dependencies.each do |explicit_dependency|
|
152
|
+
inbound_explicit_dependency_by_package[explicit_dependency] ||= []
|
153
|
+
inbound_explicit_dependency_by_package[explicit_dependency] << package.name
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
packages.each do |package| # rubocop:disable Style/CombinableLoops
|
158
|
+
package_tags = tags_for_package(package, app_name)
|
159
|
+
|
160
|
+
#
|
161
|
+
# EXPLICIT DEPENDENCIES
|
162
|
+
#
|
163
|
+
package.dependencies.each do |explicit_dependency|
|
164
|
+
to_package = ParsePackwerk.find(explicit_dependency)
|
165
|
+
if to_package.nil?
|
166
|
+
raise StandardError, "Could not find matching package #{explicit_dependency}"
|
167
|
+
end
|
168
|
+
|
169
|
+
tags = package_tags + [Tag.for('to_package', humanized_package_name(explicit_dependency))] + tags_for_to_team(CodeOwnership.for_package(to_package)&.name)
|
170
|
+
all_metrics << GaugeMetric.for('by_package.outbound_explicit_dependencies.per_package.count', 1, tags)
|
171
|
+
end
|
172
|
+
|
173
|
+
all_metrics << GaugeMetric.for('by_package.outbound_explicit_dependencies.count', package.dependencies.count, package_tags)
|
174
|
+
all_metrics << GaugeMetric.for('by_package.inbound_explicit_dependencies.count', inbound_explicit_dependency_by_package[package.name]&.count || 0, package_tags)
|
175
|
+
end
|
176
|
+
|
177
|
+
all_metrics
|
178
|
+
end
|
179
|
+
|
180
|
+
sig do
|
181
|
+
params(
|
182
|
+
all_packages: T::Array[ParsePackwerk::Package],
|
183
|
+
app_name: String
|
184
|
+
).returns(T::Array[GaugeMetric])
|
185
|
+
end
|
186
|
+
def self.get_package_metrics_by_team(all_packages, app_name)
|
187
|
+
all_metrics = T.let([], T::Array[GaugeMetric])
|
188
|
+
app_level_tag = Tag.for('app', app_name)
|
189
|
+
all_protected_packages = all_packages.map { |p| PackageProtections::ProtectedPackage.from(p) }
|
190
|
+
all_protected_packages.group_by { |protected_package| CodeOwnership.for_package(protected_package.original_package)&.name }.each do |team_name, protected_packages_by_team|
|
191
|
+
# We look at `all_packages` because we care about ALL inbound violations across all teams
|
192
|
+
inbound_violations_by_package = all_protected_packages.flat_map(&:violations).group_by(&:to_package_name)
|
193
|
+
|
194
|
+
team_tags = tags_for_team(team_name) + [app_level_tag]
|
195
|
+
all_metrics << GaugeMetric.for('by_team.all_packages.count', protected_packages_by_team.count, team_tags)
|
196
|
+
all_metrics += self.get_protections_metrics('by_team', protected_packages_by_team, team_tags)
|
197
|
+
all_metrics += self.get_public_usage_metrics('by_team', protected_packages_by_team.map(&:original_package), team_tags)
|
198
|
+
|
199
|
+
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)
|
200
|
+
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)
|
201
|
+
|
202
|
+
#
|
203
|
+
# VIOLATIONS (implicit dependencies)
|
204
|
+
#
|
205
|
+
outbound_violations = protected_packages_by_team.flat_map(&:violations)
|
206
|
+
# Here we only look at packages_by_team because we only care about inbound violations onto packages for this team
|
207
|
+
inbound_violations = protected_packages_by_team.flat_map { |package| inbound_violations_by_package[package.name] || [] }
|
208
|
+
all_dependency_violations = (outbound_violations + inbound_violations).select(&:dependency?)
|
209
|
+
all_privacy_violations = (outbound_violations + inbound_violations).select(&:privacy?)
|
210
|
+
|
211
|
+
all_metrics << GaugeMetric.for('by_team.dependency_violations.count', file_count(all_dependency_violations), team_tags)
|
212
|
+
all_metrics << GaugeMetric.for('by_team.privacy_violations.count', file_count(all_privacy_violations), team_tags)
|
213
|
+
|
214
|
+
all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.count', file_count(outbound_violations.select(&:dependency?)), team_tags)
|
215
|
+
all_metrics << GaugeMetric.for('by_team.inbound_dependency_violations.count', file_count(inbound_violations.select(&:dependency?)), team_tags)
|
216
|
+
|
217
|
+
all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.count', file_count(outbound_violations.select(&:privacy?)), team_tags)
|
218
|
+
all_metrics << GaugeMetric.for('by_team.inbound_privacy_violations.count', file_count(inbound_violations.select(&:privacy?)), team_tags)
|
219
|
+
|
220
|
+
all_metrics << GaugeMetric.for('by_team.has_readme.count', protected_packages_by_team.count { |protected_package| has_readme?(protected_package.original_package) }, team_tags)
|
221
|
+
|
222
|
+
grouped_outbound_violations = outbound_violations.group_by do |violation|
|
223
|
+
to_package = ParsePackwerk.find(violation.to_package_name)
|
224
|
+
if to_package.nil?
|
225
|
+
raise StandardError, "Could not find matching package #{violation.to_package_name}"
|
226
|
+
end
|
227
|
+
|
228
|
+
CodeOwnership.for_package(to_package)&.name
|
229
|
+
end
|
230
|
+
|
231
|
+
grouped_outbound_violations.each do |to_team_name, violations|
|
232
|
+
tags = team_tags + tags_for_to_team(to_team_name)
|
233
|
+
all_metrics << GaugeMetric.for('by_team.outbound_dependency_violations.per_team.count', file_count(violations.select(&:dependency?)), tags)
|
234
|
+
all_metrics << GaugeMetric.for('by_team.outbound_privacy_violations.per_team.count', file_count(violations.select(&:privacy?)), tags)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
all_metrics
|
239
|
+
end
|
240
|
+
|
241
|
+
private_class_method :get_package_metrics
|
242
|
+
|
243
|
+
sig do
|
244
|
+
params(
|
245
|
+
metric_name_suffix: String,
|
246
|
+
tags: T::Array[Tag],
|
247
|
+
files: T::Array[SourceCodeFile]
|
248
|
+
).returns(T::Array[GaugeMetric])
|
249
|
+
end
|
250
|
+
def self.get_file_metrics(metric_name_suffix, tags, files)
|
251
|
+
[
|
252
|
+
GaugeMetric.for("component_files.#{metric_name_suffix}", files.count(&:componentized_file?), tags),
|
253
|
+
GaugeMetric.for("packaged_files.#{metric_name_suffix}", files.count(&:packaged_file?), tags),
|
254
|
+
GaugeMetric.for("all_files.#{metric_name_suffix}", files.count, tags),
|
255
|
+
]
|
256
|
+
end
|
257
|
+
|
258
|
+
private_class_method :get_file_metrics
|
259
|
+
|
260
|
+
sig { params(prefix: String, protected_packages: T::Array[PackageProtections::ProtectedPackage], package_tags: T::Array[Tag]).returns(T::Array[GaugeMetric]) }
|
261
|
+
def self.get_protections_metrics(prefix, protected_packages, package_tags)
|
262
|
+
PackageProtections.all.flat_map do |protection|
|
263
|
+
PackageProtections::ViolationBehavior.each_value.map do |violation_behavior|
|
264
|
+
# https://github.com/Gusto/package_protections/pull/42 changed the public API of these violation behaviors.
|
265
|
+
# To preserve our ability to understand historical trends, we map to the old values.
|
266
|
+
# This allows our dashboards to continue to operate as expected.
|
267
|
+
# 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.
|
268
|
+
violation_behavior_map = {
|
269
|
+
PackageProtections::ViolationBehavior::FailOnAny => 'fail_the_build_on_any_instances',
|
270
|
+
PackageProtections::ViolationBehavior::FailNever => 'no',
|
271
|
+
PackageProtections::ViolationBehavior::FailOnNew => 'fail_the_build_if_new_instances_appear',
|
272
|
+
}
|
273
|
+
violation_behavior_name = violation_behavior_map[violation_behavior]
|
274
|
+
metric_name = "#{prefix}.#{protection.identifier}.#{violation_behavior_name}.count"
|
275
|
+
count_of_packages = protected_packages.count { |p| p.violation_behavior_for(protection.identifier) == violation_behavior }
|
276
|
+
GaugeMetric.for(metric_name, count_of_packages, package_tags)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
sig { params(prefix: String, packages: T::Array[ParsePackwerk::Package], package_tags: T::Array[Tag]).returns(T::Array[GaugeMetric]) }
|
282
|
+
def self.get_public_usage_metrics(prefix, packages, package_tags)
|
283
|
+
packages_except_for_root = packages.reject { |package| package.name == ParsePackwerk::ROOT_PACKAGE_NAME }
|
284
|
+
all_files = packages_except_for_root.flat_map do |package|
|
285
|
+
package.directory.glob('**/**.rb')
|
286
|
+
end
|
287
|
+
|
288
|
+
all_public_files = T.let([], T::Array[Pathname])
|
289
|
+
is_using_public_directory = 0
|
290
|
+
packages_except_for_root.each do |package|
|
291
|
+
public_files = package.directory.glob('app/public/**/**.rb')
|
292
|
+
all_public_files += public_files
|
293
|
+
is_using_public_directory += 1 if public_files.any?
|
294
|
+
end
|
295
|
+
|
296
|
+
# In Datadog, can divide public files by all files to get the ratio.
|
297
|
+
# This is not a metric that we are targeting -- its for observability and reflection only.
|
298
|
+
[
|
299
|
+
GaugeMetric.for("#{prefix}.all_files.count", all_files.count, package_tags),
|
300
|
+
GaugeMetric.for("#{prefix}.public_files.count", all_public_files.count, package_tags),
|
301
|
+
GaugeMetric.for("#{prefix}.using_public_directory.count", is_using_public_directory, package_tags),
|
302
|
+
]
|
303
|
+
end
|
304
|
+
|
305
|
+
sig { params(package: ParsePackwerk::Package).returns(T::Boolean) }
|
306
|
+
def self.has_readme?(package)
|
307
|
+
package.directory.join('README.md').exist?
|
308
|
+
end
|
309
|
+
|
310
|
+
sig { params(violations: T::Array[ParsePackwerk::Violation]).returns(Integer) }
|
311
|
+
def self.file_count(violations)
|
312
|
+
violations.sum { |v| v.files.count }
|
313
|
+
end
|
314
|
+
|
315
|
+
sig { params(name: String).returns(String) }
|
316
|
+
def self.humanized_package_name(name)
|
317
|
+
if name == ParsePackwerk::ROOT_PACKAGE_NAME
|
318
|
+
'root'
|
319
|
+
else
|
320
|
+
name
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module ModularizationStatistics
|
4
|
+
module Private
|
5
|
+
class SourceCodeFile < T::Struct
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
const :is_componentized_file, T::Boolean
|
9
|
+
const :is_packaged_file, T::Boolean
|
10
|
+
const :team_owner, T.nilable(Teams::Team)
|
11
|
+
const :pathname, Pathname
|
12
|
+
|
13
|
+
sig { returns(T::Boolean) }
|
14
|
+
def componentized_file?
|
15
|
+
self.is_componentized_file
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { returns(T::Boolean) }
|
19
|
+
def packaged_file?
|
20
|
+
self.is_packaged_file
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module ModularizationStatistics
|
4
|
+
class Tag < T::Struct
|
5
|
+
extend T::Sig
|
6
|
+
const :key, String
|
7
|
+
const :value, String
|
8
|
+
|
9
|
+
sig { returns(String) }
|
10
|
+
def to_s
|
11
|
+
"#{key}:#{value}"
|
12
|
+
end
|
13
|
+
|
14
|
+
sig { params(key: String, value: String).returns(Tag) }
|
15
|
+
def self.for(key, value)
|
16
|
+
new(
|
17
|
+
key: key,
|
18
|
+
value: value
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { params(other: Tag).returns(T::Boolean) }
|
23
|
+
def ==(other)
|
24
|
+
other.key == self.key &&
|
25
|
+
other.value == self.value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module ModularizationStatistics
|
4
|
+
module Tags
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
sig { params(colon_delimited_tag_strings: T::Array[String]).returns(T::Array[Tag]) }
|
8
|
+
def self.for(colon_delimited_tag_strings)
|
9
|
+
colon_delimited_tag_strings.map do |colon_delimited_tag_string|
|
10
|
+
key, value = colon_delimited_tag_string.split(':')
|
11
|
+
raise StandardError, "Improperly formatted tag `#{colon_delimited_tag_string}`" if key.nil? || value.nil?
|
12
|
+
|
13
|
+
Tag.new(
|
14
|
+
key: key,
|
15
|
+
value: value
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
require 'json'
|
5
|
+
require 'yaml'
|
6
|
+
require 'benchmark'
|
7
|
+
require 'teams'
|
8
|
+
require 'code_ownership'
|
9
|
+
require 'pathname'
|
10
|
+
require 'modularization_statistics/private/source_code_file'
|
11
|
+
require 'modularization_statistics/private/datadog_reporter'
|
12
|
+
require 'parse_packwerk'
|
13
|
+
require 'package_protections'
|
14
|
+
require 'modularization_statistics/tag'
|
15
|
+
require 'modularization_statistics/tags'
|
16
|
+
require 'modularization_statistics/gauge_metric'
|
17
|
+
|
18
|
+
module ModularizationStatistics
|
19
|
+
extend T::Sig
|
20
|
+
|
21
|
+
ROOT_PACKAGE_NAME = T.let('root'.freeze, String)
|
22
|
+
|
23
|
+
DEFAULT_COMPONENTIZED_SOURCE_CODE_LOCATIONS = T.let(
|
24
|
+
[
|
25
|
+
Pathname.new('components'),
|
26
|
+
Pathname.new('gems'),
|
27
|
+
].freeze, T::Array[Pathname]
|
28
|
+
)
|
29
|
+
|
30
|
+
DEFAULT_PACKAGED_SOURCE_CODE_LOCATIONS = T.let(
|
31
|
+
[
|
32
|
+
Pathname.new('packs'),
|
33
|
+
Pathname.new('packages'),
|
34
|
+
].freeze, T::Array[Pathname]
|
35
|
+
)
|
36
|
+
|
37
|
+
sig do
|
38
|
+
params(
|
39
|
+
datadog_client: Dogapi::Client,
|
40
|
+
app_name: String,
|
41
|
+
source_code_pathnames: T::Array[Pathname],
|
42
|
+
componentized_source_code_locations: T::Array[Pathname],
|
43
|
+
packaged_source_code_locations: T::Array[Pathname],
|
44
|
+
report_time: Time,
|
45
|
+
verbose: T::Boolean
|
46
|
+
).void
|
47
|
+
end
|
48
|
+
def self.report_to_datadog!(
|
49
|
+
datadog_client:,
|
50
|
+
app_name:,
|
51
|
+
source_code_pathnames:,
|
52
|
+
componentized_source_code_locations: DEFAULT_COMPONENTIZED_SOURCE_CODE_LOCATIONS,
|
53
|
+
packaged_source_code_locations: DEFAULT_PACKAGED_SOURCE_CODE_LOCATIONS,
|
54
|
+
report_time: Time.now, # rubocop:disable Rails/TimeZone
|
55
|
+
verbose: false
|
56
|
+
)
|
57
|
+
|
58
|
+
all_metrics = self.get_metrics(
|
59
|
+
source_code_pathnames: source_code_pathnames,
|
60
|
+
componentized_source_code_locations: componentized_source_code_locations,
|
61
|
+
packaged_source_code_locations: packaged_source_code_locations,
|
62
|
+
app_name: app_name
|
63
|
+
)
|
64
|
+
|
65
|
+
# This helps us debug what metrics are being sent
|
66
|
+
if verbose
|
67
|
+
all_metrics.each do |metric|
|
68
|
+
puts "Sending metric: #{metric}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
Private::DatadogReporter.report!(
|
73
|
+
datadog_client: datadog_client,
|
74
|
+
report_time: report_time,
|
75
|
+
metrics: all_metrics
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
sig do
|
80
|
+
params(
|
81
|
+
source_code_pathnames: T::Array[Pathname],
|
82
|
+
componentized_source_code_locations: T::Array[Pathname],
|
83
|
+
packaged_source_code_locations: T::Array[Pathname],
|
84
|
+
app_name: String
|
85
|
+
).returns(T::Array[GaugeMetric])
|
86
|
+
end
|
87
|
+
def self.get_metrics(
|
88
|
+
source_code_pathnames:,
|
89
|
+
componentized_source_code_locations:,
|
90
|
+
packaged_source_code_locations:,
|
91
|
+
app_name:
|
92
|
+
)
|
93
|
+
Private::DatadogReporter.get_metrics(
|
94
|
+
source_code_files: source_code_files(
|
95
|
+
source_code_pathnames: source_code_pathnames,
|
96
|
+
componentized_source_code_locations: componentized_source_code_locations,
|
97
|
+
packaged_source_code_locations: packaged_source_code_locations
|
98
|
+
),
|
99
|
+
app_name: app_name
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
sig do
|
104
|
+
params(
|
105
|
+
source_code_pathnames: T::Array[Pathname],
|
106
|
+
componentized_source_code_locations: T::Array[Pathname],
|
107
|
+
packaged_source_code_locations: T::Array[Pathname]
|
108
|
+
).returns(T::Array[Private::SourceCodeFile])
|
109
|
+
end
|
110
|
+
def self.source_code_files(
|
111
|
+
source_code_pathnames:,
|
112
|
+
componentized_source_code_locations:,
|
113
|
+
packaged_source_code_locations:
|
114
|
+
)
|
115
|
+
|
116
|
+
# Sorbet has the wrong signatures for `Pathname#find`, whoops!
|
117
|
+
componentized_file_set = Set.new(componentized_source_code_locations.select(&:exist?).flat_map { |pathname| T.unsafe(pathname).find.to_a })
|
118
|
+
packaged_file_set = Set.new(packaged_source_code_locations.select(&:exist?).flat_map { |pathname| T.unsafe(pathname).find.to_a })
|
119
|
+
|
120
|
+
source_code_pathnames.map do |pathname|
|
121
|
+
componentized_file = componentized_file_set.include?(pathname)
|
122
|
+
packaged_file = packaged_file_set.include?(pathname)
|
123
|
+
|
124
|
+
Private::SourceCodeFile.new(
|
125
|
+
pathname: pathname,
|
126
|
+
team_owner: CodeOwnership.for_file(pathname.to_s),
|
127
|
+
is_componentized_file: componentized_file,
|
128
|
+
is_packaged_file: packaged_file
|
129
|
+
)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private_class_method :source_code_files
|
134
|
+
end
|
data/sorbet/config
ADDED