gitlab-labkit 1.3.0 → 1.3.1
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 +4 -4
- data/.gitlab/CODEOWNERS +1 -1
- data/.gitlab-ci.yml +3 -3
- data/.pre-commit-config.yaml +1 -1
- data/CODEOWNERS +1 -1
- data/doc/FIELD_STANDARDIZATION.md +249 -0
- data/exe/labkit-logging +198 -0
- data/gitlab-labkit.gemspec +2 -0
- data/lib/labkit/logging/field_validator/config.rb +150 -0
- data/lib/labkit/logging/field_validator/log_interceptor.rb +89 -0
- data/lib/labkit/logging/field_validator/registry.rb +115 -0
- data/lib/labkit/logging/field_validator.rb +178 -0
- data/lib/labkit/logging.rb +4 -0
- metadata +10 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 888cc5479aac46e57a89719bf13d9403a83f4d92c4f6c2a280b4c71950fee250
|
|
4
|
+
data.tar.gz: 2d5cc94125a609627065b10faa15a85eec3998285686db906a3ce1f54ed1f89d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc655d31560edac3cc29db253c3ea9c20f5fddfc479ac6f76563d56dfd795f265b0119a4480fa9bd10f90c9652eba31f458625a48570b70119004e4bf8ff609f
|
|
7
|
+
data.tar.gz: 992a1a49adecaf7a52bb4caaec4484dc8feddcfc88fc83a8ddb4ada3b0aca0f6b50de32bf6a64062c9380482958175ff237a037d3c61023e4e541cb1efbd2230
|
data/.gitlab/CODEOWNERS
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
* @andrewn @ayufan @
|
|
1
|
+
* @reprazent @andrewn @mkaeppler @ayufan @hmerscher @d.barrett @splattael
|
data/.gitlab-ci.yml
CHANGED
|
@@ -19,13 +19,13 @@ include:
|
|
|
19
19
|
# It includes standard checks, gitlab-scanners, validations and release processes
|
|
20
20
|
# common to all projects using this template library.
|
|
21
21
|
# see https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/blob/main/templates/standard.md
|
|
22
|
-
- component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/standard-build@v3.
|
|
22
|
+
- component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/standard-build@v3.5
|
|
23
23
|
|
|
24
24
|
# Runs rspec tests and rubocop on the project
|
|
25
25
|
# see https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/blob/main/templates/ruby.md
|
|
26
|
-
- component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/ruby-build@v3.
|
|
26
|
+
- component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/ruby-build@v3.5
|
|
27
27
|
|
|
28
|
-
- component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/danger@v3.
|
|
28
|
+
- component: $CI_SERVER_FQDN/gitlab-com/gl-infra/common-ci-tasks/danger@v3.5
|
|
29
29
|
|
|
30
30
|
.test_template: &test_definition
|
|
31
31
|
extends: .with_bundle
|
data/.pre-commit-config.yaml
CHANGED
|
@@ -25,7 +25,7 @@ repos:
|
|
|
25
25
|
# Documentation available at
|
|
26
26
|
# https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/blob/main/docs/pre-commit.md
|
|
27
27
|
- repo: https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks
|
|
28
|
-
rev: v3.
|
|
28
|
+
rev: v3.5 # renovate:managed
|
|
29
29
|
|
|
30
30
|
hooks:
|
|
31
31
|
- id: shellcheck # Run shellcheck for changed Shell files
|
data/CODEOWNERS
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
# CODEOWNERS is used to lookup assignees for
|
|
2
2
|
# Renovate Bot dependency change Merge Requests.
|
|
3
3
|
# https://docs.renovatebot.com/configuration-options/#assigneesfromcodeowners
|
|
4
|
-
* @reprazent @andrewn @mkaeppler @ayufan @hmerscher
|
|
4
|
+
* @reprazent @andrewn @mkaeppler @ayufan @hmerscher @d.barrett @splattael
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# LabKit Field Standardization
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The LabKit Field Validator detects when your code uses deprecated logging field names and helps you migrate to standardized fields. This supports the [Observability Field Standardisation initiative](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/observability_field_standardisation/).
|
|
6
|
+
|
|
7
|
+
**Goal:** Standardize logging field names across GitLab so logs are queryable and actionable across all systems.
|
|
8
|
+
|
|
9
|
+
**How it works:** The validator intercepts logging calls during development and testing, detects deprecated fields, and compares them against a frozen baseline. New offenses fail CI; known offenses are tracked in `.labkit_logging_todo.yml`. The validator is **not** active in production environments.
|
|
10
|
+
|
|
11
|
+
For the architectural decision and rationale, see [ADR: Dynamic Runtime Linting](./architecture/decisions/001_field_standardization_dynamic_runtime_linting.md).
|
|
12
|
+
|
|
13
|
+
## Key Concepts
|
|
14
|
+
|
|
15
|
+
**Offense**
|
|
16
|
+
- A unique combination of [File Path] + [Deprecated Field] + [Logger Class]
|
|
17
|
+
- Multiple log calls in the same file using the same deprecated field = 1 offense
|
|
18
|
+
- Offenses exist until the deprecated field is entirely removed from the file
|
|
19
|
+
|
|
20
|
+
**TODO Baseline**
|
|
21
|
+
- A list of known offenses tracked in `.labkit_logging_todo.yml`
|
|
22
|
+
- Existing offenses in this baseline are allowed
|
|
23
|
+
- Any new offenses detected during development raise an error
|
|
24
|
+
- Prevents regression while allowing incremental cleanup
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### First-Time Setup
|
|
29
|
+
|
|
30
|
+
1. **Initialize the todo file:**
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bundle exec labkit-logging init
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This creates `.labkit_logging_todo.yml` with `skip_ci_failure: true`, which allows CI to pass while collecting the initial baseline.
|
|
37
|
+
|
|
38
|
+
2. **Commit and push:**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git add .labkit_logging_todo.yml
|
|
42
|
+
git commit -m "Add LabKit logging todo baseline"
|
|
43
|
+
git push
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
3. **Wait for CI to complete**, then fetch the baseline:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bundle exec labkit-logging fetch <project> <pipeline_id>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For example:
|
|
53
|
+
```bash
|
|
54
|
+
bundle exec labkit-logging fetch gitlab-org/gitlab 12345
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This fetches all detected offenses from the CI pipeline logs and populates the todo file. The `skip_ci_failure` flag is automatically removed.
|
|
58
|
+
|
|
59
|
+
4. **Commit the populated baseline:**
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git add .labkit_logging_todo.yml
|
|
63
|
+
git commit -m "Populate LabKit logging todo baseline"
|
|
64
|
+
git push
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Future CI runs will now enforce this baseline—new offenses will fail the pipeline.
|
|
68
|
+
|
|
69
|
+
## Developer Workflow
|
|
70
|
+
|
|
71
|
+
### Fixing Offenses (Recommended)
|
|
72
|
+
|
|
73
|
+
Replace deprecated fields with standard constants:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
# Before
|
|
77
|
+
logger.info(user_id: current_user.id)
|
|
78
|
+
|
|
79
|
+
# After
|
|
80
|
+
logger.info(Labkit::Fields::GL_USER_ID => current_user.id)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
When you fix an offense, it's automatically removed from the baseline on the next test run. Run your tests locally to verify:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
LABKIT_LOGGING_TODO_UPDATE=true bundle exec rspec
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Adding Offenses Temporarily
|
|
90
|
+
|
|
91
|
+
If you can't fix an offense immediately, add it to the baseline:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
LABKIT_LOGGING_TODO_UPDATE=true bundle exec rspec
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This updates `.labkit_logging_todo.yml` with any new offenses found during the test run. Commit the updated file with your changes.
|
|
98
|
+
|
|
99
|
+
**Note:** Justify in your MR why you can't fix immediately. Keep the baseline as small as possible.
|
|
100
|
+
|
|
101
|
+
### Regenerating the Baseline
|
|
102
|
+
|
|
103
|
+
To regenerate the entire baseline from scratch:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
rm .labkit_logging_todo.yml
|
|
107
|
+
bundle exec labkit-logging init
|
|
108
|
+
# Run CI, then:
|
|
109
|
+
bundle exec labkit-logging fetch <project> <pipeline_id>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## CI Behavior
|
|
113
|
+
|
|
114
|
+
### Baseline Generation Mode
|
|
115
|
+
|
|
116
|
+
When `skip_ci_failure: true` is set in the todo file:
|
|
117
|
+
|
|
118
|
+
- CI passes even when deprecated fields are detected
|
|
119
|
+
- Offenses are logged for collection via `labkit-logging fetch`
|
|
120
|
+
- Use this mode only during initial setup
|
|
121
|
+
|
|
122
|
+
### Enforcement Mode
|
|
123
|
+
|
|
124
|
+
When `skip_ci_failure` is not set (normal operation):
|
|
125
|
+
|
|
126
|
+
- **New offenses fail the pipeline** with a detailed error message
|
|
127
|
+
- **Known offenses** (in the baseline) are allowed
|
|
128
|
+
- **Fixed offenses** are automatically detected and can be removed from the baseline
|
|
129
|
+
|
|
130
|
+
Example CI failure output:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
================================================================================
|
|
134
|
+
LabKit Logging Field Standardization: New Offenses Detected
|
|
135
|
+
================================================================================
|
|
136
|
+
|
|
137
|
+
app/services/user_service.rb:42: 'user_id' is deprecated. Use 'Labkit::Fields::GL_USER_ID' instead.
|
|
138
|
+
app/models/project.rb:15: 'project_id' is deprecated. Use 'Labkit::Fields::GL_PROJECT_ID' instead.
|
|
139
|
+
|
|
140
|
+
================================================================================
|
|
141
|
+
Total: 2 new offense(s) in 2 file(s)
|
|
142
|
+
================================================================================
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### When Offenses Are Fixed
|
|
146
|
+
|
|
147
|
+
When you fix offenses that were in the baseline, you'll see a message indicating which offenses were resolved. Update the baseline locally to remove them:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
LABKIT_LOGGING_TODO_UPDATE=true bundle exec rspec
|
|
151
|
+
git add .labkit_logging_todo.yml
|
|
152
|
+
git commit -m "Remove fixed logging offenses from baseline"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## CLI Reference
|
|
156
|
+
|
|
157
|
+
The `labkit-logging` command provides subcommands for managing the field validator.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
bundle exec labkit-logging <command> [options]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### labkit-logging init
|
|
164
|
+
|
|
165
|
+
Creates a new `.labkit_logging_todo.yml` file with `skip_ci_failure: true`.
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
bundle exec labkit-logging init
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### labkit-logging fetch
|
|
172
|
+
|
|
173
|
+
Fetches offense logs from a GitLab CI pipeline and updates the todo file.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
bundle exec labkit-logging fetch <project> <pipeline_id>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Arguments:**
|
|
180
|
+
- `project` - GitLab project ID or path (e.g., `278964` or `gitlab-org/gitlab`)
|
|
181
|
+
- `pipeline_id` - CI pipeline ID number
|
|
182
|
+
|
|
183
|
+
**Environment Variables:**
|
|
184
|
+
- `GITLAB_TOKEN` - GitLab API token (required)
|
|
185
|
+
- `CI_API_V4_URL` - GitLab API URL (default: `https://gitlab.com/api/v4`)
|
|
186
|
+
|
|
187
|
+
**Examples:**
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Using project path
|
|
191
|
+
bundle exec labkit-logging fetch gitlab-org/gitlab 12345
|
|
192
|
+
|
|
193
|
+
# Using project ID
|
|
194
|
+
bundle exec labkit-logging fetch 278964 12345
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Environment Variables
|
|
198
|
+
|
|
199
|
+
| Variable | Description |
|
|
200
|
+
|----------|-------------|
|
|
201
|
+
| `LABKIT_LOGGING_TODO_UPDATE=true` | Update the baseline with new offenses (local development) |
|
|
202
|
+
| `GITLAB_TOKEN` | GitLab API token for fetching CI logs |
|
|
203
|
+
| `CI_API_V4_URL` | GitLab API URL (defaults to gitlab.com) |
|
|
204
|
+
|
|
205
|
+
## Troubleshooting
|
|
206
|
+
|
|
207
|
+
**"New Offenses Detected" in CI**
|
|
208
|
+
- Fix the deprecated fields in your code, or
|
|
209
|
+
- Update the baseline locally: `LABKIT_LOGGING_TODO_UPDATE=true bundle exec rspec`
|
|
210
|
+
- Commit the updated `.labkit_logging_todo.yml`
|
|
211
|
+
|
|
212
|
+
**Offenses not detected**
|
|
213
|
+
- Ensure `.labkit_logging_todo.yml` exists in your project root
|
|
214
|
+
- Verify you're using `Labkit::Logging::JsonLogger`
|
|
215
|
+
- Check you're logging with a Hash (not String)
|
|
216
|
+
- Verify the code path is executed during tests
|
|
217
|
+
|
|
218
|
+
**Pipeline not found when fetching**
|
|
219
|
+
- Verify the project path/ID is correct
|
|
220
|
+
- Ensure the pipeline has completed (not still running)
|
|
221
|
+
- Check your `GITLAB_TOKEN` has read access to the project
|
|
222
|
+
|
|
223
|
+
**No offenses found in pipeline**
|
|
224
|
+
- Ensure `skip_ci_failure: true` was set during the CI run
|
|
225
|
+
- Verify the pipeline ran tests that exercise the logging code
|
|
226
|
+
- Check job logs are accessible with your token
|
|
227
|
+
|
|
228
|
+
## TODO File Format
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
# LabKit Logging Field Standardization TODO
|
|
232
|
+
# AUTO-GENERATED FILE. DO NOT EDIT MANUALLY.
|
|
233
|
+
|
|
234
|
+
offenses:
|
|
235
|
+
- logger_class: "Labkit::Logging::JsonLogger"
|
|
236
|
+
callsite: "app/services/user_service.rb"
|
|
237
|
+
deprecated_field: "user_id"
|
|
238
|
+
standard_field: "Labkit::Fields::GL_USER_ID"
|
|
239
|
+
- logger_class: "Labkit::Logging::JsonLogger"
|
|
240
|
+
callsite: "app/models/project.rb"
|
|
241
|
+
deprecated_field: "project_id"
|
|
242
|
+
standard_field: "Labkit::Fields::GL_PROJECT_ID"
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## References
|
|
246
|
+
|
|
247
|
+
- [ADR: Dynamic Runtime Linting](./architecture/decisions/001_field_standardization_dynamic_runtime_linting.md)
|
|
248
|
+
- [Observability Field Standardisation](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/observability_field_standardisation/)
|
|
249
|
+
- [Quality Epic](https://gitlab.com/groups/gitlab-org/quality/-/epics/235)
|
data/exe/labkit-logging
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'yaml'
|
|
8
|
+
require 'set'
|
|
9
|
+
require 'labkit/fields'
|
|
10
|
+
require 'labkit/logging/field_validator/config'
|
|
11
|
+
|
|
12
|
+
module Labkit
|
|
13
|
+
module Logging
|
|
14
|
+
module FieldValidator
|
|
15
|
+
class CLI
|
|
16
|
+
THREAD_POOL_SIZE = 10
|
|
17
|
+
DETECTED_OFFENSE_PREFIX = 'LABKIT_LOGGING_OFFENSE'
|
|
18
|
+
|
|
19
|
+
def run(args)
|
|
20
|
+
case args.shift
|
|
21
|
+
when 'init' then init_todo_file
|
|
22
|
+
when 'fetch' then fetch(args)
|
|
23
|
+
else puts "Usage: labkit-logging <init|fetch> [options]\n\n init Initialize .labkit_logging_todo.yml\n fetch Fetch offenses: labkit-logging fetch <project> <pipeline_id>"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def init_todo_file
|
|
28
|
+
Config.init_file!
|
|
29
|
+
warn "Created #{Config.file_name} with skip_ci_failure enabled.\n\nNext steps:\n1. Commit this file\n2. Push and let CI run\n3. Run: labkit-logging fetch <project> <pipeline_id>"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def fetch(args)
|
|
35
|
+
@project = args[0]
|
|
36
|
+
@pipeline = args[1]
|
|
37
|
+
abort "Usage: labkit-logging fetch <project> <pipeline_id>" unless @project && @pipeline&.match?(/^\d+$/)
|
|
38
|
+
abort "GITLAB_TOKEN environment variable is required" unless token
|
|
39
|
+
|
|
40
|
+
warn "Fetching offenses from pipeline #{@pipeline}..."
|
|
41
|
+
validate_pipeline!
|
|
42
|
+
jobs = fetch_jobs
|
|
43
|
+
|
|
44
|
+
detected = process_jobs(jobs)
|
|
45
|
+
new_off, removed_off = compute_diff(detected)
|
|
46
|
+
|
|
47
|
+
if new_off.empty? && removed_off.empty?
|
|
48
|
+
warn "\nNo changes detected."
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
save_results(new_off, removed_off)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def validate_pipeline!
|
|
56
|
+
resp = api_get("/projects/#{enc(@project)}/pipelines/#{@pipeline}")
|
|
57
|
+
abort "Pipeline not found" unless resp.is_a?(Net::HTTPSuccess)
|
|
58
|
+
status = JSON.parse(resp.body)['status']
|
|
59
|
+
abort "Pipeline still running" if status == 'running'
|
|
60
|
+
abort "Pipeline status: #{status}" unless %w[success failed].include?(status)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def fetch_jobs
|
|
64
|
+
jobs = []
|
|
65
|
+
page = 1
|
|
66
|
+
loop do
|
|
67
|
+
resp = api_get("/projects/#{enc(@project)}/pipelines/#{@pipeline}/jobs?per_page=100&page=#{page}")
|
|
68
|
+
abort "Failed to fetch jobs" unless resp.is_a?(Net::HTTPSuccess)
|
|
69
|
+
batch = JSON.parse(resp.body)
|
|
70
|
+
break if batch.empty?
|
|
71
|
+
|
|
72
|
+
jobs.concat(batch)
|
|
73
|
+
page += 1
|
|
74
|
+
end
|
|
75
|
+
jobs
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def process_jobs(jobs)
|
|
79
|
+
detected = []
|
|
80
|
+
mutex = Mutex.new
|
|
81
|
+
queue = Queue.new
|
|
82
|
+
jobs.each { |j| queue << j }
|
|
83
|
+
total = jobs.size
|
|
84
|
+
done = 0
|
|
85
|
+
|
|
86
|
+
threads = Array.new(THREAD_POOL_SIZE) do
|
|
87
|
+
Thread.new do
|
|
88
|
+
loop do
|
|
89
|
+
job = begin
|
|
90
|
+
queue.pop(true)
|
|
91
|
+
rescue StandardError
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
break unless job
|
|
95
|
+
|
|
96
|
+
begin
|
|
97
|
+
resp = api_get("/projects/#{enc(@project)}/jobs/#{job['id']}/trace")
|
|
98
|
+
if resp.is_a?(Net::HTTPSuccess)
|
|
99
|
+
offenses = parse_log(resp.body)
|
|
100
|
+
mutex.synchronize { detected.concat(offenses) }
|
|
101
|
+
end
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
mutex.synchronize { warn "\nWarning: #{job['name']}: #{e.message}" }
|
|
104
|
+
ensure
|
|
105
|
+
mutex.synchronize do
|
|
106
|
+
done += 1
|
|
107
|
+
$stderr.print "\rProcessing jobs: #{done}/#{total}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
threads.each(&:join)
|
|
114
|
+
warn ""
|
|
115
|
+
|
|
116
|
+
dedupe(detected)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def parse_log(log)
|
|
120
|
+
offenses = []
|
|
121
|
+
log.each_line do |line|
|
|
122
|
+
clean = line.gsub(/\e\[[0-9;]*[a-zA-Z]/, '').strip
|
|
123
|
+
next unless clean.include?(DETECTED_OFFENSE_PREFIX)
|
|
124
|
+
|
|
125
|
+
idx = clean.index(DETECTED_OFFENSE_PREFIX)
|
|
126
|
+
next unless idx
|
|
127
|
+
|
|
128
|
+
json = clean[(idx + DETECTED_OFFENSE_PREFIX.length)..].sub(/^[:\s]+/, '')
|
|
129
|
+
offense = begin
|
|
130
|
+
JSON.parse(json)
|
|
131
|
+
rescue StandardError
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
offenses << offense if offense
|
|
135
|
+
end
|
|
136
|
+
offenses
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def compute_diff(detected)
|
|
140
|
+
baseline = Config.load.fetch('offenses', [])
|
|
141
|
+
|
|
142
|
+
baseline_keys = baseline.to_set { |o| [o['callsite'], o['deprecated_field'], o['logger_class']] }
|
|
143
|
+
detected_keys = detected.to_set { |o| [o['callsite'], o['deprecated_field'], o['logger_class']] }
|
|
144
|
+
|
|
145
|
+
new_offenses = detected.reject do |o|
|
|
146
|
+
key = [o['callsite'], o['deprecated_field'], o['logger_class']]
|
|
147
|
+
baseline_keys.include?(key)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
removed_offenses = baseline.select do |o|
|
|
151
|
+
key = [o['callsite'], o['deprecated_field'], o['logger_class']]
|
|
152
|
+
!detected_keys.include?(key) # rubocop:disable Rails/NegateInclude -- Set has no exclude? method
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
[new_offenses, removed_offenses]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def dedupe(offenses)
|
|
159
|
+
offenses.uniq { |o| [o['callsite'], o['deprecated_field'], o['logger_class']] }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def save_results(new_off, removed_off)
|
|
163
|
+
skip_removed = Config.load.fetch('skip_ci_failure', false)
|
|
164
|
+
updated = Config.update!(new_off, removed_off)
|
|
165
|
+
warn "\n✓ Added #{new_off.size} new offenses" if new_off.any?
|
|
166
|
+
warn "✓ Removed #{removed_off.size} fixed offenses" if removed_off.any?
|
|
167
|
+
warn "✓ Removed skip_ci_failure flag" if skip_removed
|
|
168
|
+
warn "✓ Total: #{updated.size} offenses\n\nCommit the updated #{Config.file_name} file."
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def api_get(path)
|
|
172
|
+
uri = URI.parse("#{api_url}#{path}")
|
|
173
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
174
|
+
http.use_ssl = uri.scheme == 'https'
|
|
175
|
+
http.open_timeout = 10
|
|
176
|
+
http.read_timeout = 30
|
|
177
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
178
|
+
req['PRIVATE-TOKEN'] = token
|
|
179
|
+
http.request(req)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def token
|
|
183
|
+
ENV['GITLAB_API_PRIVATE_TOKEN'] || ENV['GITLAB_TOKEN'] || ENV.fetch('CI_JOB_TOKEN', nil)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def api_url
|
|
187
|
+
ENV['GITLAB_API_ENDPOINT'] || ENV['CI_API_V4_URL'] || 'https://gitlab.com/api/v4'
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def enc(str)
|
|
191
|
+
URI.encode_www_form_component(str.to_s)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
Labkit::Logging::FieldValidator::CLI.new.run(ARGV)
|
data/gitlab-labkit.gemspec
CHANGED
|
@@ -16,6 +16,8 @@ Gem::Specification.new do |spec|
|
|
|
16
16
|
spec.license = "MIT"
|
|
17
17
|
|
|
18
18
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|tools)/}) }
|
|
19
|
+
spec.bindir = "exe"
|
|
20
|
+
spec.executables = %w[labkit-logging]
|
|
19
21
|
spec.require_paths = ["lib"]
|
|
20
22
|
spec.required_ruby_version = "~> 3.2"
|
|
21
23
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Labkit
|
|
4
|
+
module Logging
|
|
5
|
+
module FieldValidator
|
|
6
|
+
module Config
|
|
7
|
+
FILE_NAME = '.labkit_logging_todo.yml'
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def root
|
|
11
|
+
@root ||= detect_root.freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def path
|
|
15
|
+
@path ||= File.join(root, file_name).freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def file_name
|
|
19
|
+
FILE_NAME
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def deprecated_fields
|
|
23
|
+
@deprecated_fields ||= Labkit::Fields::Deprecated.all.freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def load
|
|
27
|
+
return {} unless config_file_exists?
|
|
28
|
+
|
|
29
|
+
YAML.safe_load_file(path) || {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update!(new_offenses, removed_offenses = [])
|
|
33
|
+
baseline_offenses = load.fetch('offenses', [])
|
|
34
|
+
|
|
35
|
+
if removed_offenses.any?
|
|
36
|
+
removed_keys = removed_offenses.to_set { |o| offense_key(o) }
|
|
37
|
+
baseline_offenses = baseline_offenses.reject { |o| removed_keys.include?(offense_key(o)) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
updated = process_offenses(baseline_offenses + new_offenses)
|
|
41
|
+
write_file({ 'offenses' => updated }, header: header)
|
|
42
|
+
updated
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def init_file!
|
|
46
|
+
return if config_file_exists?
|
|
47
|
+
|
|
48
|
+
content = <<~YAML
|
|
49
|
+
# LabKit Logging Field Standardization TODO
|
|
50
|
+
# This file tracks deprecated logging fields that need migration.
|
|
51
|
+
#
|
|
52
|
+
# To collect offenses from CI:
|
|
53
|
+
# bundle exec labkit-logging fetch <project> <pipeline_id>
|
|
54
|
+
#
|
|
55
|
+
# To collect offenses locally:
|
|
56
|
+
# LABKIT_LOGGING_TODO_UPDATE=true <local development process>
|
|
57
|
+
#
|
|
58
|
+
# More info: https://gitlab.com/gitlab-org/ruby/gems/labkit-ruby/-/blob/master/doc/FIELD_STANDARDIZATION.md
|
|
59
|
+
|
|
60
|
+
skip_ci_failure: true # Remove this flag once baseline is established
|
|
61
|
+
|
|
62
|
+
offenses: []
|
|
63
|
+
YAML
|
|
64
|
+
|
|
65
|
+
write_file(nil, header: content)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def config_file_exists?
|
|
69
|
+
File.exist?(path)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def skip_ci_failure?
|
|
73
|
+
load.fetch('skip_ci_failure', false) == true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def detect_root
|
|
77
|
+
return Bundler.root.to_s if defined?(Bundler) && Bundler.respond_to?(:root)
|
|
78
|
+
return Rails.root.to_s if defined?(Rails) && Rails.respond_to?(:root)
|
|
79
|
+
|
|
80
|
+
Dir.pwd
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def header
|
|
84
|
+
<<~HEADER
|
|
85
|
+
# LabKit Logging Field Standardization TODO
|
|
86
|
+
# AUTO-GENERATED FILE. DO NOT EDIT MANUALLY.
|
|
87
|
+
#
|
|
88
|
+
# This file tracks deprecated logging fields that need to be migrated to standard fields.
|
|
89
|
+
# Each offense represents a file using a deprecated field that should be replaced.
|
|
90
|
+
#
|
|
91
|
+
# === HOW TO FIX ===
|
|
92
|
+
#
|
|
93
|
+
# 1. Replace the deprecated field with the standard field constant
|
|
94
|
+
# 2. Remove the deprecated field entirely (adding the new field is not enough)
|
|
95
|
+
# 3. Run your tests - the offense will be automatically removed
|
|
96
|
+
#
|
|
97
|
+
# Example:
|
|
98
|
+
# # Before
|
|
99
|
+
# logger.info(user_id: 123)
|
|
100
|
+
#
|
|
101
|
+
# # After
|
|
102
|
+
# logger.info(Labkit::Fields::GL_USER_ID => 123)
|
|
103
|
+
#
|
|
104
|
+
# === ADDING OFFENSES (if fixing is not immediately possible) ===
|
|
105
|
+
#
|
|
106
|
+
# Run: LABKIT_LOGGING_TODO_UPDATE=true bundle exec rspec <spec_file>
|
|
107
|
+
#
|
|
108
|
+
# === REGENERATE ENTIRE TODO ===
|
|
109
|
+
#
|
|
110
|
+
# Delete this file and run: LABKIT_LOGGING_TODO_UPDATE=true bundle exec rspec
|
|
111
|
+
|
|
112
|
+
HEADER
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def write_file(data, header: self.header)
|
|
118
|
+
content = data ? header + YAML.dump(data, line_width: -1) : header
|
|
119
|
+
File.write(path, content)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def offense_key(offense)
|
|
123
|
+
[offense['callsite'], offense['deprecated_field'], offense['logger_class']]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def process_offenses(offenses)
|
|
127
|
+
offenses
|
|
128
|
+
.uniq { |o| offense_key(o) }
|
|
129
|
+
.sort_by { |o| offense_key(o) }
|
|
130
|
+
.map { |o| format_offense(o) }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def format_offense(offense)
|
|
134
|
+
{
|
|
135
|
+
'logger_class' => offense['logger_class'],
|
|
136
|
+
'callsite' => offense['callsite'],
|
|
137
|
+
'deprecated_field' => offense['deprecated_field'],
|
|
138
|
+
'standard_field' => standard_field_constant(offense['standard_field'])
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def standard_field_constant(standard_field)
|
|
143
|
+
const_name = Labkit::Fields.constant_name_for(standard_field)
|
|
144
|
+
const_name ? "Labkit::Fields::#{const_name}" : standard_field
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Labkit
|
|
4
|
+
module Logging
|
|
5
|
+
module FieldValidator
|
|
6
|
+
module LogInterceptor
|
|
7
|
+
IGNORE_PATHS = [
|
|
8
|
+
%r{/gems/logger-},
|
|
9
|
+
%r{/lib/labkit/logging/},
|
|
10
|
+
%r{/gems/rspec-},
|
|
11
|
+
%r{/ruby/\d+\.\d+\.\d+/}
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
LOGGER_WRAPPER_PATTERNS = [
|
|
15
|
+
%r{/.*logger\.rb$}
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def register_wrapper_pattern(pattern)
|
|
20
|
+
wrapper_patterns << pattern
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def wrapper_patterns
|
|
24
|
+
@wrapper_patterns ||= LOGGER_WRAPPER_PATTERNS.dup
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reset_wrapper_patterns!
|
|
28
|
+
@wrapper_patterns = nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def format_data(severity, timestamp, progname, message)
|
|
33
|
+
data = super
|
|
34
|
+
location = determine_callsite
|
|
35
|
+
|
|
36
|
+
return data unless location
|
|
37
|
+
|
|
38
|
+
callsite_path = normalize_path(location.path)
|
|
39
|
+
return data unless callsite_path
|
|
40
|
+
|
|
41
|
+
all_fields = extract_string_keys(data)
|
|
42
|
+
logger_class = self.class.name || 'AnonymousLogger'
|
|
43
|
+
|
|
44
|
+
all_fields.each do |field|
|
|
45
|
+
standard_field = Labkit::Fields::Deprecated.standard_field_for(field)
|
|
46
|
+
next unless standard_field
|
|
47
|
+
|
|
48
|
+
Registry.instance.record_offense(callsite_path, location.lineno, field, standard_field, logger_class)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Registry.instance.check_for_removed_offenses(callsite_path, all_fields, logger_class)
|
|
52
|
+
|
|
53
|
+
data
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def determine_callsite
|
|
59
|
+
locations = caller_locations(1, 30) || []
|
|
60
|
+
|
|
61
|
+
locations.find do |loc|
|
|
62
|
+
path = loc.path
|
|
63
|
+
|
|
64
|
+
next if path == __FILE__
|
|
65
|
+
next if IGNORE_PATHS.any? { |pattern| pattern.match?(path) }
|
|
66
|
+
next if LogInterceptor.wrapper_patterns.any? { |pattern| pattern.match?(path) }
|
|
67
|
+
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def normalize_path(absolute_path)
|
|
73
|
+
root = Config.root
|
|
74
|
+
return nil unless absolute_path.start_with?(root)
|
|
75
|
+
|
|
76
|
+
start_idx = root.length
|
|
77
|
+
start_idx += 1 if absolute_path[start_idx] == '/'
|
|
78
|
+
absolute_path[start_idx..]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def extract_string_keys(data)
|
|
82
|
+
return Set.new unless data.is_a?(Hash)
|
|
83
|
+
|
|
84
|
+
data.keys.to_set(&:to_s)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'singleton'
|
|
4
|
+
require 'set'
|
|
5
|
+
|
|
6
|
+
module Labkit
|
|
7
|
+
module Logging
|
|
8
|
+
module FieldValidator
|
|
9
|
+
class Registry
|
|
10
|
+
include Singleton
|
|
11
|
+
|
|
12
|
+
attr_reader :offenses
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
@offenses = Set.new
|
|
17
|
+
@offense_keys = Set.new # For O(1) lookup
|
|
18
|
+
@removed_offenses = Set.new
|
|
19
|
+
@baseline_by_callsite = nil
|
|
20
|
+
@resolved_fields = {} # Cache for resolved standard fields
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def record_offense(callsite, lineno, deprecated_field, standard_field, logger_class)
|
|
24
|
+
key = [callsite, deprecated_field, logger_class].freeze
|
|
25
|
+
|
|
26
|
+
return if @offense_keys.include?(key)
|
|
27
|
+
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
next if @offense_keys.include?(key)
|
|
30
|
+
|
|
31
|
+
@offense_keys << key
|
|
32
|
+
@offenses << {
|
|
33
|
+
'callsite' => callsite,
|
|
34
|
+
'lineno' => lineno,
|
|
35
|
+
'deprecated_field' => deprecated_field,
|
|
36
|
+
'standard_field' => standard_field,
|
|
37
|
+
'logger_class' => logger_class
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
@removed_offenses.reject! { |r| offense_key(r) == key }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def check_for_removed_offenses(callsite, fields, logger_class)
|
|
45
|
+
baseline = baseline_by_callsite[[callsite, logger_class]]
|
|
46
|
+
return unless baseline
|
|
47
|
+
|
|
48
|
+
@mutex.synchronize do
|
|
49
|
+
baseline.each do |offense|
|
|
50
|
+
key = offense_key(offense)
|
|
51
|
+
next if @offense_keys.include?(key)
|
|
52
|
+
|
|
53
|
+
deprecated = offense['deprecated_field']
|
|
54
|
+
standard = resolve_standard_field(offense['standard_field'])
|
|
55
|
+
|
|
56
|
+
next unless fields.include?(standard) && fields.exclude?(deprecated)
|
|
57
|
+
|
|
58
|
+
@removed_offenses << offense
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns [detected_offenses, new_offenses, removed_offenses]
|
|
64
|
+
def finalize
|
|
65
|
+
@mutex.synchronize do
|
|
66
|
+
baseline_keys = load_baseline.to_set { |b| offense_key(b) }
|
|
67
|
+
|
|
68
|
+
new_offenses = @offenses.reject { |o| baseline_keys.include?(offense_key(o)) }
|
|
69
|
+
|
|
70
|
+
[@offenses.to_a, new_offenses, @removed_offenses.to_a]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def clear!
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
@offenses.clear
|
|
77
|
+
@offense_keys.clear
|
|
78
|
+
@removed_offenses.clear
|
|
79
|
+
@baseline_by_callsite = nil
|
|
80
|
+
@resolved_fields.clear
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def offense_key(offense)
|
|
87
|
+
[offense['callsite'], offense['deprecated_field'], offense['logger_class']].freeze
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def baseline_by_callsite
|
|
91
|
+
@baseline_by_callsite ||= load_baseline.group_by { |o| [o['callsite'], o['logger_class']] }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def resolve_standard_field(standard_field)
|
|
95
|
+
# Convert "Labkit::Fields::GL_USER_ID" to actual value "gl.user_id"
|
|
96
|
+
@resolved_fields[standard_field] ||= resolve_labkit_field_constant(standard_field)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def resolve_labkit_field_constant(standard_field)
|
|
100
|
+
return standard_field unless standard_field.start_with?('Labkit::Fields::')
|
|
101
|
+
|
|
102
|
+
const_name = standard_field.delete_prefix('Labkit::Fields::')
|
|
103
|
+
return standard_field unless Labkit::Fields.const_defined?(const_name)
|
|
104
|
+
|
|
105
|
+
Labkit::Fields.const_get(const_name)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def load_baseline
|
|
109
|
+
config = Config.load
|
|
110
|
+
config.fetch('offenses', [])
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
|
|
7
|
+
require_relative 'field_validator/config'
|
|
8
|
+
require_relative 'field_validator/log_interceptor'
|
|
9
|
+
require_relative 'field_validator/registry'
|
|
10
|
+
|
|
11
|
+
module Labkit
|
|
12
|
+
module Logging
|
|
13
|
+
##
|
|
14
|
+
# Runtime validator for logging fields.
|
|
15
|
+
# Validates logged fields against standard and deprecated field lists.
|
|
16
|
+
# This validator is automatically injected in non-production environments.
|
|
17
|
+
# Offenses are collected during test runs and development.
|
|
18
|
+
module FieldValidator
|
|
19
|
+
class << self
|
|
20
|
+
# Inject the validator into JsonLogger
|
|
21
|
+
def inject!
|
|
22
|
+
return if @injected
|
|
23
|
+
|
|
24
|
+
# If the config file does not exist, we don't inject the validator.
|
|
25
|
+
# To enable field validation in a repository, a config file must exist.
|
|
26
|
+
return unless Config.config_file_exists?
|
|
27
|
+
|
|
28
|
+
::Labkit::Logging::JsonLogger.prepend(LogInterceptor)
|
|
29
|
+
Kernel.at_exit { FieldValidator.process_violations }
|
|
30
|
+
@injected = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize_todo_file
|
|
34
|
+
Config.init_file!
|
|
35
|
+
warn "Created .labkit_logging_todo.yml with skip_ci_failure enabled."
|
|
36
|
+
warn ""
|
|
37
|
+
warn "Next steps:"
|
|
38
|
+
warn "1. Commit this file to source control"
|
|
39
|
+
warn "2. Push and let CI run to generate offense logs"
|
|
40
|
+
warn "3. Run: bundle exec labkit-logging fetch <project> <pipeline_id>"
|
|
41
|
+
warn "4. Commit the populated todo file (skip_ci_failure will be removed automatically)"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def process_violations
|
|
45
|
+
detected_offenses, new_offenses, removed_offenses = Registry.instance.finalize
|
|
46
|
+
|
|
47
|
+
return if detected_offenses.empty? && new_offenses.empty? && removed_offenses.empty?
|
|
48
|
+
|
|
49
|
+
in_ci = ENV['CI'] == 'true'
|
|
50
|
+
|
|
51
|
+
output_ndjson(detected_offenses) if in_ci
|
|
52
|
+
|
|
53
|
+
# Auto-remove fixed offenses (not in CI to avoid race conditions)
|
|
54
|
+
handle_removed_offenses(removed_offenses) if removed_offenses.any? && !in_ci
|
|
55
|
+
|
|
56
|
+
if ENV['LABKIT_LOGGING_TODO_UPDATE'] == 'true'
|
|
57
|
+
handle_update(new_offenses)
|
|
58
|
+
elsif new_offenses.any?
|
|
59
|
+
handle_new_offenses(new_offenses)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def clear_offenses!
|
|
64
|
+
Registry.instance.clear!
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def handle_removed_offenses(removed_offenses)
|
|
70
|
+
Config.update!([], removed_offenses)
|
|
71
|
+
|
|
72
|
+
warn thank_you_message(removed_offenses)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def thank_you_message(removed_offenses)
|
|
76
|
+
lines = [
|
|
77
|
+
"",
|
|
78
|
+
"=" * 80,
|
|
79
|
+
"Thank you for improving our logging standards!",
|
|
80
|
+
"=" * 80,
|
|
81
|
+
"",
|
|
82
|
+
"You fixed #{removed_offenses.size} deprecated field offense(s):",
|
|
83
|
+
""
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
removed_offenses.each do |o|
|
|
87
|
+
lines << " - #{o['callsite']}: '#{o['deprecated_field']}' -> '#{format_standard_field(o['standard_field'])}'"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
lines << ""
|
|
91
|
+
lines << "#{Config.file_name} has been automatically updated."
|
|
92
|
+
lines << "Please commit the changes to complete the cleanup."
|
|
93
|
+
lines << ""
|
|
94
|
+
lines << ("=" * 80)
|
|
95
|
+
lines << ""
|
|
96
|
+
|
|
97
|
+
lines.join("\n")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def handle_new_offenses(new_offenses)
|
|
101
|
+
if ENV['CI'] == 'true' && Config.skip_ci_failure?
|
|
102
|
+
warn baseline_generation_message(new_offenses)
|
|
103
|
+
else
|
|
104
|
+
warn report_new_offenses(new_offenses)
|
|
105
|
+
raise "New LabKit logging offenses detected"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def baseline_generation_message(offenses)
|
|
110
|
+
lines = [
|
|
111
|
+
"",
|
|
112
|
+
"ℹ️ LabKit Logging: Baseline generation mode active",
|
|
113
|
+
"",
|
|
114
|
+
"Deprecated fields detected but skip_ci_failure is enabled.",
|
|
115
|
+
"Offenses are being logged for collection.",
|
|
116
|
+
"",
|
|
117
|
+
"To establish baseline:",
|
|
118
|
+
" bundle exec labkit-logging fetch <project> <pipeline_id>",
|
|
119
|
+
"",
|
|
120
|
+
"Documentation: https://gitlab.com/gitlab-org/ruby/gems/labkit-ruby/-/blob/master/doc/FIELD_STANDARDIZATION.md",
|
|
121
|
+
"",
|
|
122
|
+
"--- Offenses Summary ---",
|
|
123
|
+
"Total offenses: #{offenses.size} across #{offenses.map { |o| o['callsite'] }.uniq.size} file(s)",
|
|
124
|
+
""
|
|
125
|
+
]
|
|
126
|
+
lines.join("\n")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def output_ndjson(detected_offenses)
|
|
130
|
+
detected_offenses.each do |offense|
|
|
131
|
+
puts "LABKIT_LOGGING_OFFENSE: #{JSON.generate(offense)}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def handle_update(new_offenses)
|
|
136
|
+
updated_offenses = Config.update!(new_offenses)
|
|
137
|
+
|
|
138
|
+
warn "\n✓ Updated .labkit_logging_todo.yml"
|
|
139
|
+
warn "\n Added #{new_offenses.size} new offenses ✓" if new_offenses.any?
|
|
140
|
+
|
|
141
|
+
warn "Total: #{updated_offenses.size} offenses"
|
|
142
|
+
warn "\nCommit the updated #{Config.file_name}."
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def report_new_offenses(new_offenses)
|
|
146
|
+
lines = [
|
|
147
|
+
"",
|
|
148
|
+
"=" * 80,
|
|
149
|
+
"LabKit Logging Field Standardization: New Offenses Detected",
|
|
150
|
+
"=" * 80,
|
|
151
|
+
""
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
new_offenses.each do |o|
|
|
155
|
+
lines << "#{o['callsite']}:#{o['lineno']}: '#{o['deprecated_field']}' is deprecated. Use '#{format_standard_field(o['standard_field'])}' instead."
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
lines << ""
|
|
159
|
+
lines << ("=" * 80)
|
|
160
|
+
lines << "Total: #{new_offenses.size} new offense(s) in #{new_offenses.map { |o| o['callsite'] }.uniq.size} file(s)"
|
|
161
|
+
lines << ""
|
|
162
|
+
lines << "See https://gitlab.com/gitlab-org/ruby/gems/labkit-ruby/-/blob/master/doc/FIELD_STANDARDIZATION.md"
|
|
163
|
+
lines << ("=" * 80)
|
|
164
|
+
lines << ""
|
|
165
|
+
|
|
166
|
+
lines.join("\n")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def format_standard_field(standard_field)
|
|
170
|
+
const_name = Labkit::Fields.constant_name_for(standard_field)
|
|
171
|
+
const_name ? "Labkit::Fields::#{const_name}" : standard_field
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
Labkit::Logging::FieldValidator.inject!
|
data/lib/labkit/logging.rb
CHANGED
|
@@ -7,5 +7,9 @@ module Labkit
|
|
|
7
7
|
autoload :GRPC, "labkit/logging/grpc"
|
|
8
8
|
autoload :Sanitizer, "labkit/logging/sanitizer"
|
|
9
9
|
autoload :JsonLogger, "labkit/logging/json_logger"
|
|
10
|
+
|
|
11
|
+
# Eagerly load FieldValidator in non-production environments
|
|
12
|
+
# This ensures injection happens before JsonLogger instances are created
|
|
13
|
+
require "labkit/logging/field_validator" unless ENV['RAILS_ENV'] == 'production'
|
|
10
14
|
end
|
|
11
15
|
end
|
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitlab-labkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Newdigate
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
@@ -451,7 +451,8 @@ dependencies:
|
|
|
451
451
|
version: 1.7.0
|
|
452
452
|
email:
|
|
453
453
|
- andrew@gitlab.com
|
|
454
|
-
executables:
|
|
454
|
+
executables:
|
|
455
|
+
- labkit-logging
|
|
455
456
|
extensions: []
|
|
456
457
|
extra_rdoc_files: []
|
|
457
458
|
files:
|
|
@@ -481,7 +482,9 @@ files:
|
|
|
481
482
|
- Rakefile
|
|
482
483
|
- config/user_experience_slis/schema.json
|
|
483
484
|
- config/user_experience_slis/testing_sample.yml
|
|
485
|
+
- doc/FIELD_STANDARDIZATION.md
|
|
484
486
|
- doc/architecture/decisions/001_field_standardization_dynamic_runtime_linting.md
|
|
487
|
+
- exe/labkit-logging
|
|
485
488
|
- gitlab-labkit.gemspec
|
|
486
489
|
- lib/gitlab-labkit.rb
|
|
487
490
|
- lib/labkit/context.rb
|
|
@@ -498,6 +501,10 @@ files:
|
|
|
498
501
|
- lib/labkit/json_schema/README.md
|
|
499
502
|
- lib/labkit/json_schema/ref_resolver.rb
|
|
500
503
|
- lib/labkit/logging.rb
|
|
504
|
+
- lib/labkit/logging/field_validator.rb
|
|
505
|
+
- lib/labkit/logging/field_validator/config.rb
|
|
506
|
+
- lib/labkit/logging/field_validator/log_interceptor.rb
|
|
507
|
+
- lib/labkit/logging/field_validator/registry.rb
|
|
501
508
|
- lib/labkit/logging/grpc.rb
|
|
502
509
|
- lib/labkit/logging/grpc/server_interceptor.rb
|
|
503
510
|
- lib/labkit/logging/json_logger.rb
|