提交 3ef95534 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 6be19afa
<svg height="82" viewBox="0 0 78 82" width="78" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><g fill-rule="nonzero"><path d="m2.12 42c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z" fill="#000" fill-opacity=".03"/><path d="m39 78c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35s-15.67-35-35-35-35 15.67-35 35 15.67 35 35 35z" fill="#eee"/><path d="m44 31-2.5-3-2.5 3-2.5-3-2.5 3-2.5-3-2.5 3h-2.72c2.65-4.2 7.36-7 12.72-7s10.07 2.8 12.72 7h-2.72l-2.5-3z" fill="#e1dbf2"/><path d="m39 57c-9.4 0-17-7.6-17-17s7.6-17 17-17 17 7.6 17 17-7.6 17-17 17zm0-4c7.18 0 13-5.82 13-13s-5.82-13-13-13-13 5.82-13 13 5.82 13 13 13z" fill="#e1dbf2"/></g><g fill="#e2ba3e" stroke="#fff" transform="translate(14 18)"><path d="m8.5 12.75-4.99617464 2.6266445.95418445-5.56332227-4.0419902-3.93996668 5.58589307-.81167778 2.49808732-5.06167777 2.4980873 5.06167777 5.5858931.81167778-4.0419902 3.93996668.9541844 5.56332227z"/><path d="m24.5 12.75-4.9961746 2.6266445.9541844-5.56332227-4.0419902-3.93996668 5.5858931-.81167778 2.4980873-5.06167777 2.4980873 5.06167777 5.5858931.81167778-4.0419902 3.93996668.9541844 5.56332227z"/><path d="m40.5 12.75-4.9961746 2.6266445.9541844-5.56332227-4.0419902-3.93996668 5.5858931-.81167778 2.4980873-5.06167777 2.4980873 5.06167777 5.5858931.81167778-4.0419902 3.93996668.9541844 5.56332227z"/></g><path d="m35 45h8c0 2.2-1.8 4-4 4s-4-1.8-4-4zm-1.5-2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z" fill="#6b4fbb" fill-rule="nonzero"/></g></svg>
\ No newline at end of file
<svg width="78" height="82" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z" fill-opacity=".03" fill="#000" fill-rule="nonzero"/><path d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z" fill="#EEE" fill-rule="nonzero"/><path d="M44 31l-2.5-3-2.5 3-2.5-3-2.5 3-2.5-3-2.5 3h-2.72c2.65-4.2 7.36-7 12.72-7s10.07 2.8 12.72 7H49l-2.5-3-2.5 3z" fill="#E1DBF2" fill-rule="nonzero"/><path d="M39 57c-9.4 0-17-7.6-17-17s7.6-17 17-17 17 7.6 17 17-7.6 17-17 17zm0-4c7.18 0 13-5.82 13-13s-5.82-13-13-13-13 5.82-13 13 5.82 13 13 13z" fill="#E1DBF2" fill-rule="nonzero"/><g transform="translate(20 13)"><path d="M5.731 15.24V8.736H33.75v6.504c0 4.851-6.272 8.784-14.01 8.784-7.737 0-14.009-3.933-14.009-8.784z" fill="#0B2630"/><path d="M.75 7.662L18.824.182a2.4 2.4 0 0 1 1.835 0l18.072 7.48a1.2 1.2 0 0 1 0 2.218l-18.072 7.48a2.4 2.4 0 0 1-1.835 0L.75 9.88a1.2 1.2 0 0 1 0-2.218z" fill="#29424E"/><path d="M19.295 9.771a1.194 1.194 0 0 1-.66-1.557c.248-.612.948-.907 1.562-.659l11.516 4.657v11.766c0 .66-.538 1.195-1.2 1.195-.663 0-1.2-.535-1.2-1.195V13.822l-10.018-4.05z" fill="#FFCF00" fill-rule="nonzero"/><path d="M32.613 23.373v3.807h-4.2v-3.807c0-.711.353-1.34.894-1.72h2.411c.541.38.895 1.009.895 1.72z" fill="#FCA326"/><path fill="#FFCF00" d="M28.413 23.592H32.613V27.18H28.413z"/></g><path d="M35 45h8c0 2.2-1.8 4-4 4s-4-1.8-4-4zm-1.5-2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z" fill="#6B4FBB" fill-rule="nonzero"/></g></svg>
\ No newline at end of file
......@@ -15,11 +15,13 @@ import { s__ } from '~/locale';
import query from '../graphql/queries/details.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ALERTS_SEVERITY_LABELS, trackAlertsDetailsViewsOptions } from '../constants';
import createIssueQuery from '../graphql/mutations/create_issue_from_alert.graphql';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
import { toggleContainerClasses } from '~/lib/utils/dom_utils';
import SystemNote from './system_notes/system_note.vue';
import AlertSidebar from './alert_sidebar.vue';
const containerEl = document.querySelector('.page-with-contextual-sidebar');
......@@ -47,7 +49,9 @@ export default {
GlTable,
TimeAgoTooltip,
AlertSidebar,
SystemNote,
},
mixins: [glFeatureFlagsMixin()],
props: {
alertId: {
type: String,
......@@ -159,6 +163,9 @@ export default {
const { category, action } = trackAlertsDetailsViewsOptions;
Tracking.event(category, action);
},
alertRefresh() {
this.$apollo.queries.alert.refetch();
},
},
};
</script>
......@@ -287,6 +294,13 @@ export default {
</div>
<div class="gl-pl-2" data-testid="service">{{ alert.service }}</div>
</div>
<template v-if="glFeatures.alertAssignee">
<div v-if="alert.notes" class="issuable-discussion">
<ul class="notes main-notes-list timeline">
<system-note v-for="note in alert.notes.nodes" :key="note.id" :note="note" />
</ul>
</div>
</template>
</gl-tab>
<gl-tab data-testid="fullDetailsTab" :title="$options.i18n.fullAlertDetailsTitle">
<gl-table
......@@ -309,6 +323,7 @@ export default {
:project-path="projectPath"
:alert="alert"
:sidebar-collapsed="sidebarCollapsed"
@alert-refresh="alertRefresh"
@toggle-sidebar="toggleSidebar"
@alert-sidebar-error="handleAlertSidebarError"
/>
......
......@@ -53,6 +53,7 @@ export default {
v-if="glFeatures.alertAssignee"
:project-path="projectPath"
:alert="alert"
@alert-refresh="$emit('alert-refresh')"
@toggle-sidebar="$emit('toggle-sidebar')"
@alert-sidebar-error="$emit('alert-sidebar-error', $event)"
/>
......
......@@ -141,6 +141,7 @@ export default {
})
.then(() => {
this.hideDropdown();
this.$emit('alert-refresh');
})
.catch(() => {
this.$emit('alert-sidebar-error', this.$options.UPDATE_ALERT_ASSIGNEES_ERROR);
......
<script>
import NoteHeader from '~/notes/components/note_header.vue';
import { spriteIcon } from '~/lib/utils/common_utils';
export default {
components: {
NoteHeader,
},
props: {
note: {
type: Object,
required: true,
},
},
computed: {
noteAnchorId() {
return `note_${this.note?.id?.split('/').pop()}`;
},
iconHtml() {
return spriteIcon('user');
},
},
};
</script>
<template>
<li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper">
<div class="timeline-entry-inner">
<div class="timeline-icon" v-html="iconHtml"></div>
<div class="timeline-content">
<div class="note-header">
<note-header :author="note.author" :created-at="note.createdAt" :note-id="note.id">
<span v-html="note.bodyHtml"></span>
</note-header>
</div>
</div>
</div>
</li>
</template>
#import "~/graphql_shared/fragments/author.fragment.graphql"
fragment AlertNote on Note {
id
author {
id
state
...Author
}
body
bodyHtml
createdAt
discussion {
id
}
}
#import "./list_item.fragment.graphql"
#import "./alert_note.fragment.graphql"
fragment AlertDetailItem on AlertManagementAlert {
...AlertListItem
......@@ -8,4 +9,9 @@ fragment AlertDetailItem on AlertManagementAlert {
description
updatedAt
details
notes {
nodes {
...AlertNote
}
}
}
#import "../fragments/detailItem.fragment.graphql"
#import "../fragments/detail_item.fragment.graphql"
query alertDetails($fullPath: ID!, $alertId: String) {
project(fullPath: $fullPath) {
......
......@@ -135,7 +135,7 @@ hr {
text-overflow: ellipsis;
white-space: nowrap;
> div:not(.block),
> div:not(.block):not(.select2-display-none),
.str-truncated {
display: inline;
}
......
......@@ -58,6 +58,42 @@
}
}
// Essentially we’re doing @include form-control-focus here (from
// bootstrap/scss/mixins/_forms.scss), except that the bootstrap mixin adds a
// `&:focus` selector and we’re never actually focusing the .select2-choice
// link nor the .select2-container, the Select2 library focuses an off-screen
// .select2-focusser element instead.
&.select2-container-active:not(.select2-dropdown-open) {
.select2-choice {
color: $input-focus-color;
background-color: $input-focus-bg;
border-color: $input-focus-border-color;
outline: 0;
}
// Reusable focus “glow” box-shadow
@mixin form-control-focus-glow {
@if $enable-shadows {
box-shadow: $input-box-shadow, $input-focus-box-shadow;
} @else {
box-shadow: $input-focus-box-shadow;
}
}
// Apply the focus “glow” shadow to the .select2-container if it also has
// the .block-truncated class as that applies an overflow: hidden, thereby
// hiding the glow of the nested .select2-choice element.
&.block-truncated {
@include form-control-focus-glow;
}
// Apply the glow directly to the .select2-choice link if we’re not
// block-truncating the container.
&:not(.block-truncated) .select2-choice {
@include form-control-focus-glow;
}
}
&.is-invalid {
~ .invalid-feedback {
display: block;
......
.signup-page[data-page^='registrations:experience_levels'] {
$card-shadow-color: rgba($black, 0.2);
.page-wrap {
background-color: $white;
}
.card-deck {
max-width: 828px;
}
.card {
transition: box-shadow 0.3s ease-in-out;
}
.card:hover {
box-shadow: 0 $gl-spacing-scale-3 $gl-spacing-scale-5 $card-shadow-color;
}
@media (min-width: $breakpoint-sm) {
.card-deck .card {
margin: 0 $gl-spacing-scale-3;
}
}
.stretched-link:hover {
text-decoration: none;
}
}
......@@ -89,3 +89,14 @@
.gl-text-purple { color: $purple; }
.gl-bg-purple-light { background-color: $purple-light; }
// move this to GitLab UI once onboarding experiment is considered a success
.gl-py-8 {
padding-top: $gl-spacing-scale-8;
padding-bottom: $gl-spacing-scale-8;
}
// move this to GitLab UI once onboarding experiment is considered a success
.gl-pl-7 {
padding-left: $gl-spacing-scale-7;
}
......@@ -6,6 +6,8 @@ module Types
graphql_name 'AlertManagementAlert'
description "Describes an alert from the project's Alert Management"
implements(Types::Notes::NoteableType)
authorize :read_alert_management_alert
field :iid,
......
......@@ -19,6 +19,8 @@ module Types
Types::SnippetType
when ::DesignManagement::Design
Types::DesignManagement::DesignType
when ::AlertManagement::Alert
Types::AlertManagement::AlertType
else
raise "Unknown GraphQL type for #{object}"
end
......
......@@ -8,6 +8,7 @@ module AlertManagement
include AtomicInternalId
include ShaAttribute
include Sortable
include Noteable
include Gitlab::SQL::Pattern
STATUSES = {
......@@ -30,6 +31,9 @@ module AlertManagement
has_many :alert_assignees, inverse_of: :alert
has_many :assignees, through: :alert_assignees
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :user_mentions, class_name: 'AlertManagement::AlertUserMention', foreign_key: :alert_management_alert_id
has_internal_id :iid, scope: :project, init: ->(s) { s.project.alert_management_alerts.maximum(:iid) }
sha_attribute :fingerprint
......
# frozen_string_literal: true
module AlertManagement
class AlertUserMention < UserMention
belongs_to :alert_management_alert, class_name: '::AlertManagement::Alert'
belongs_to :note
end
end
......@@ -274,6 +274,10 @@ class Note < ApplicationRecord
noteable_type == "Snippet"
end
def for_alert_mangement_alert?
noteable_type == 'AlertManagement::Alert'
end
def for_personal_snippet?
noteable.is_a?(PersonalSnippet)
end
......@@ -396,7 +400,13 @@ class Note < ApplicationRecord
end
def noteable_ability_name
for_snippet? ? 'snippet' : noteable_type.demodulize.underscore
if for_snippet?
'snippet'
elsif for_alert_mangement_alert?
'alert_management_alert'
else
noteable_type.demodulize.underscore
end
end
def can_be_discussion_note?
......
......@@ -19,9 +19,11 @@ module AlertManagement
return error_no_updates if params.empty?
filter_assignees
old_assignees = alert.assignees.to_a
if alert.update(params)
assign_todo
process_assignement(old_assignees)
success
else
error(alert.errors.full_messages.to_sentence)
......@@ -32,29 +34,10 @@ module AlertManagement
attr_reader :alert, :current_user, :params
def assign_todo
return unless assignee
todo_service.assign_alert(alert, assignee)
end
def allowed?
current_user.can?(:update_alert_management_alert, alert)
end
def filter_assignees
return if params[:assignees].nil?
params[:assignees] = Array(assignee)
end
def assignee
strong_memoize(:assignee) do
# Take first assignee while multiple are not currently supported
params[:assignees]&.first
end
end
def todo_service
strong_memoize(:todo_service) do
TodoService.new
......@@ -76,6 +59,35 @@ module AlertManagement
def error_no_updates
error(_('Please provide attributes to update'))
end
# ----- Assignee-related behavior ------
def filter_assignees
return if params[:assignees].nil?
params[:assignees] = Array(assignee)
end
def assignee
strong_memoize(:assignee) do
# Take first assignee while multiple are not currently supported
params[:assignees]&.first
end
end
def process_assignement(old_assignees)
assign_todo
add_assignee_system_note(old_assignees)
end
def assign_todo
return unless assignee
todo_service.assign_alert(alert, assignee)
end
def add_assignee_system_note(old_assignees)
SystemNoteService.change_issuable_assignees(alert, alert.project, current_user, old_assignees)
end
end
end
end
......@@ -23,7 +23,7 @@
display_path: true,
extra_group: namespace_id),
{},
{ class: 'select2 js-select-namespace qa-project-namespace-select block-truncated', tabindex: 1, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_path", track_value: "" }})
{ class: 'select2 js-select-namespace qa-project-namespace-select block-truncated', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_path", track_value: "" }})
- else
.input-group-prepend.static-namespace.flex-shrink-0.has-tooltip{ title: user_url(current_user.username) + '/' }
......
- page_title _('What’s your experience level?')
%h3= _('Hello there')
%p= _('Welcome to the guided GitLab tour')
.gl-display-flex.gl-flex-direction-column.gl-align-items-center
= image_tag 'learn-gitlab-avatar.jpg', width: '90'
%br
%h2.gl-text-center.gl-mt-3.gl-mb-3= _('Hello there')
%p.gl-text-center.gl-font-lg.gl-mb-6= _('Welcome to the guided GitLab tour')
%h5= _('What describes you best?')
%h3.gl-text-center.gl-font-lg.gl-mt-6.gl-mb-0= _('What describes you best?')
%hr
.card-deck.gl-mt-6
.card
.card-body.gl-display-flex.gl-py-8.gl-pr-5.gl-pl-7
.gl-align-self-center.gl-pr-6
= image_tag 'novice.svg', width: '78', height: '78', alt: ''
%div
%p.gl-font-lg.gl-font-weight-bold.gl-mb-2= _('Novice')
%p= _('I’m not very familiar with the basics of project management and DevOps.')
= link_to _('Show me everything'), users_sign_up_experience_level_path(experience_level: :novice, namespace_path: params[:namespace_path]), method: :patch, class: 'stretched-link'
%div
%p
%b= _('Novice')
%p= _('I’m not very familiar with the basics of project management and DevOps.')
%p
= link_to _('Show me everything'), users_sign_up_experience_level_path(experience_level: :novice, namespace_path: params[:namespace_path]), method: :patch
%hr
%div
%p
%b= _('Experienced')
%p= _('I’m familiar with the basics of project management and DevOps.')
%p
= link_to _('Show me more advanced stuff'), users_sign_up_experience_level_path(experience_level: :experienced, namespace_path: params[:namespace_path]), method: :patch
.card
.card-body.gl-display-flex.gl-py-8.gl-pr-5.gl-pl-7
.gl-align-self-center.gl-pr-6
= image_tag 'experienced.svg', width: '78', height: '78', alt: ''
%div
%p.gl-font-lg.gl-font-weight-bold.gl-mb-2= _('Experienced')
%p= _('I’m familiar with the basics of project management and DevOps.')
= link_to _('Show me more advanced stuff'), users_sign_up_experience_level_path(experience_level: :experienced, namespace_path: params[:namespace_path]), method: :patch, class: 'stretched-link'
......@@ -9,6 +9,7 @@ class AuthorizedKeysWorker
urgency :high
weight 2
idempotent!
loggable_arguments 0
def perform(action, *args)
return unless Gitlab::CurrentSettings.authorized_keys_enabled?
......
......@@ -5,6 +5,7 @@ class BackgroundMigrationWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :database
urgency :throttled
loggable_arguments 0, 1
# The minimum amount of time between processing two jobs of the same migration
# class.
......
......@@ -5,6 +5,7 @@ class CleanupContainerRepositoryWorker # rubocop:disable Scalability/IdempotentW
queue_namespace :container_repository
feature_category :container_registry
loggable_arguments 2
attr_reader :container_repository, :current_user
......
......@@ -6,6 +6,7 @@ class ClusterInstallAppWorker # rubocop:disable Scalability/IdempotentWorker
include ClusterApplications
worker_has_external_dependencies!
loggable_arguments 0
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
......
......@@ -6,6 +6,7 @@ class ClusterPatchAppWorker # rubocop:disable Scalability/IdempotentWorker
include ClusterApplications
worker_has_external_dependencies!
loggable_arguments 0
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
......
......@@ -9,6 +9,7 @@ class ClusterUpdateAppWorker # rubocop:disable Scalability/IdempotentWorker
include ExclusiveLeaseGuard
sidekiq_options retry: 3, dead: false
loggable_arguments 0, 3
LEASE_TIMEOUT = 10.minutes.to_i
......
......@@ -6,6 +6,7 @@ class ClusterUpgradeAppWorker # rubocop:disable Scalability/IdempotentWorker
include ClusterApplications
worker_has_external_dependencies!
loggable_arguments 0
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
......
......@@ -10,6 +10,7 @@ class ClusterWaitForAppInstallationWorker # rubocop:disable Scalability/Idempote
worker_has_external_dependencies!
worker_resource_boundary :cpu
loggable_arguments 0
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
......
......@@ -8,6 +8,8 @@ class ClusterWaitForAppUpdateWorker # rubocop:disable Scalability/IdempotentWork
INTERVAL = 10.seconds
TIMEOUT = 20.minutes
loggable_arguments 0
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
::Clusters::Applications::CheckUpgradeProgressService.new(app).execute
......
......@@ -6,6 +6,7 @@ class ClusterWaitForIngressIpAddressWorker # rubocop:disable Scalability/Idempot
include ClusterApplications
worker_has_external_dependencies!
loggable_arguments 0
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
......
......@@ -6,6 +6,8 @@ module Clusters
include ApplicationWorker
include ClusterQueue
loggable_arguments 1
def perform(cluster_id, service_name)
cluster = Clusters::Cluster.find_by_id(cluster_id)
return unless cluster
......
......@@ -6,6 +6,8 @@ module Clusters
include ApplicationWorker
include ClusterQueue
loggable_arguments 1
def perform(cluster_id, service_name)
cluster = Clusters::Cluster.find_by_id(cluster_id)
raise cluster_missing_error(service_name) unless cluster
......
......@@ -8,6 +8,7 @@ module Clusters
include ClusterApplications
worker_has_external_dependencies!
loggable_arguments 0
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
......
......@@ -12,6 +12,7 @@ module Clusters
worker_has_external_dependencies!
worker_resource_boundary :cpu
loggable_arguments 0
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
......
......@@ -76,6 +76,22 @@ module ApplicationWorker
get_sidekiq_options['queue'].to_s
end
# Set/get which arguments can be logged and sent to Sentry.
#
# Numeric arguments are logged by default, so there is no need to
# list those.
#
# Non-numeric arguments must be listed by position, as Sidekiq
# cannot see argument names.
#
def loggable_arguments(*args)
if args.any?
@loggable_arguments = args
else
@loggable_arguments || []
end
end
def queue_size
Sidekiq::Queue.new(queue).size
end
......
......@@ -7,6 +7,7 @@ module ReactiveCacheableWorker
include ApplicationWorker
feature_category_not_owned!
loggable_arguments 0
def self.context_for_arguments(arguments)
class_name, *_other_args = arguments
......
......@@ -5,8 +5,8 @@ class CreateCommitSignatureWorker
feature_category :source_code_management
weight 2
idempotent!
loggable_arguments 0
# rubocop: disable CodeReuse/ActiveRecord
def perform(commit_shas, project_id)
......
......@@ -8,6 +8,7 @@ class CreatePipelineWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :continuous_integration
urgency :high
worker_resource_boundary :cpu
loggable_arguments 2, 3, 4
def perform(project_id, user_id, ref, source, params = {})
project = Project.find(project_id)
......
......@@ -4,6 +4,7 @@ class DeleteStoredFilesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
feature_category_not_owned!
loggable_arguments 0
def perform(class_name, keys)
klass = begin
......
......@@ -4,6 +4,7 @@ class DeleteUserWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
feature_category :authentication_and_authorization
loggable_arguments 2
def perform(current_user_id, delete_user_id, options = {})
delete_user = User.find(delete_user_id)
......
......@@ -5,6 +5,7 @@ class ExportCsvWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :issue_tracking
worker_resource_boundary :cpu
loggable_arguments 2
def perform(current_user_id, project_id, params)
@current_user = User.find(current_user_id)
......
......@@ -5,6 +5,7 @@ class FileHookWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: false
feature_category :integrations
loggable_arguments 0
def perform(file_name, data)
success, message = Gitlab::FileHook.execute(file_name, data)
......
......@@ -5,6 +5,7 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: false
feature_category :gitaly
loggable_arguments 1, 2, 3
# Timeout set to 24h
LEASE_TIMEOUT = 86400
......
......@@ -12,6 +12,7 @@ module Gitlab
sidekiq_options dead: false
feature_category :importers
loggable_arguments 1, 2
# The known importer stages and their corresponding Sidekiq workers.
STAGES = {
......
......@@ -8,6 +8,8 @@ module Gitlab
include Gitlab::JiraImport::QueueOptions
include Gitlab::Import::DatabaseHelpers
loggable_arguments 3
def perform(project_id, jira_issue_id, issue_attributes, waiter_key)
issue_id = create_issue(issue_attributes, project_id)
JiraImport.cache_issue_mapping(issue_id, jira_issue_id, project_id)
......
......@@ -7,6 +7,7 @@ class GitlabShellWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :source_code_management
urgency :high
weight 2
loggable_arguments 0
def perform(action, *arg)
# Gitlab::Shell is being removed but we need to continue to process jobs
......
......@@ -5,6 +5,7 @@ class GroupExportWorker # rubocop:disable Scalability/IdempotentWorker
include ExceptionBacktrace
feature_category :importers
loggable_arguments 2
def perform(current_user_id, group_id, params = {})
current_user = User.find(current_user_id)
......
......@@ -5,6 +5,7 @@ module HashedStorage
include ApplicationWorker
queue_namespace :hashed_storage
loggable_arguments 1
attr_reader :project_id
......
......@@ -5,6 +5,7 @@ module HashedStorage
include ApplicationWorker
queue_namespace :hashed_storage
loggable_arguments 1
attr_reader :project_id
......
......@@ -9,6 +9,7 @@ module MailScheduler
feature_category :issue_tracking
worker_resource_boundary :cpu
loggable_arguments 0
def perform(meth, *args)
check_arguments!(args)
......
......@@ -6,6 +6,7 @@ class MergeWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :source_code_management
urgency :high
weight 5
loggable_arguments 2
def perform(merge_request_id, current_user_id, params)
params = params.with_indifferent_access
......
......@@ -7,6 +7,7 @@ module ObjectStorage
sidekiq_options retry: 5
feature_category_not_owned!
loggable_arguments 0, 1, 2, 3
def perform(uploader_class_name, subject_class_name, file_field, subject_id)
uploader_class = uploader_class_name.constantize
......
......@@ -7,6 +7,7 @@ module ObjectStorage
include ObjectStorageQueue
feature_category_not_owned!
loggable_arguments 0, 1, 2, 3
SanityCheckError = Class.new(StandardError)
......
......@@ -5,6 +5,7 @@ class PagesWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: 3
feature_category :pages
loggable_arguments 0, 1
def perform(action, *arg)
send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
......
......@@ -7,6 +7,7 @@ class PipelineProcessWorker # rubocop:disable Scalability/IdempotentWorker
queue_namespace :pipeline_processing
feature_category :continuous_integration
urgency :high
loggable_arguments 1
# rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id, build_ids = nil)
......
......@@ -7,6 +7,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
urgency :high
worker_resource_boundary :cpu
weight 5
loggable_arguments 0, 1, 2, 3
def perform(gl_repository, identifier, changes, push_options = {})
container, project, repo_type = Gitlab::GlRepository.parse(gl_repository)
......
......@@ -13,8 +13,8 @@ class ProcessCommitWorker
feature_category :source_code_management
urgency :high
weight 3
idempotent!
loggable_arguments 2, 3
# project_id - The ID of the project this commit belongs to.
# user_id - The ID of the user that pushed the commit.
......
......@@ -4,11 +4,11 @@
class ProjectCacheWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
urgency :high
LEASE_TIMEOUT = 15.minutes.to_i
feature_category :source_code_management
urgency :high
loggable_arguments 1, 2, 3
# project_id - The ID of the project for which to flush the cache.
# files - An Array containing extra types of files to refresh such as
......
......@@ -8,6 +8,7 @@ class ProjectExportWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :importers
worker_resource_boundary :memory
urgency :throttled
loggable_arguments 2, 3
def perform(current_user_id, project_id, after_export_strategy = {}, params = {})
current_user = User.find(current_user_id)
......
......@@ -4,8 +4,8 @@ class PropagateIntegrationWorker
include ApplicationWorker
feature_category :integrations
idempotent!
loggable_arguments 1
def perform(integration_id, overwrite)
Admin::PropagateIntegrationService.propagate(
......
......@@ -7,6 +7,7 @@ class RebaseWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :source_code_management
weight 2
loggable_arguments 2
def perform(merge_request_id, current_user_id, skip_ci = false)
current_user = User.find(current_user_id)
......
......@@ -12,6 +12,8 @@ module RepositoryCheck
attr_reader :shard_name
loggable_arguments 0
def perform(shard_name)
@shard_name = shard_name
......
......@@ -5,6 +5,7 @@ class RepositoryRemoveRemoteWorker # rubocop:disable Scalability/IdempotentWorke
include ExclusiveLeaseGuard
feature_category :source_code_management
loggable_arguments 1
LEASE_TIMEOUT = 1.hour
......
......@@ -10,6 +10,7 @@ class RepositoryUpdateRemoteMirrorWorker # rubocop:disable Scalability/Idempoten
sidekiq_options retry: 3, dead: false
feature_category :source_code_management
loggable_arguments 1
LOCK_WAIT_TIME = 30.seconds
MAX_TRIES = 3
......
......@@ -5,6 +5,8 @@ module TodosDestroyer
include ApplicationWorker
include TodosDestroyerQueue
loggable_arguments 2
def perform(user_id, entity_id, entity_type)
::Todos::Destroy::EntityLeaveService.new(user_id, entity_id, entity_type).execute
end
......
......@@ -5,6 +5,7 @@ class UpdateExternalPullRequestsWorker # rubocop:disable Scalability/IdempotentW
feature_category :source_code_management
weight 3
loggable_arguments 2
def perform(project_id, user_id, ref)
project = Project.find_by_id(project_id)
......
......@@ -7,6 +7,7 @@ class UpdateMergeRequestsWorker # rubocop:disable Scalability/IdempotentWorker
urgency :high
worker_resource_boundary :cpu
weight 3
loggable_arguments 2, 3, 4
LOG_TIME_THRESHOLD = 90 # seconds
......
......@@ -5,6 +5,7 @@ class WebHookWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :integrations
worker_has_external_dependencies!
loggable_arguments 2
sidekiq_options retry: 4, dead: false
......
---
title: Fix tabbing through form fields in projects/new flow
merge_request: 33209
author:
type: fixed
---
title: Add system note when assigning user to alert
merge_request: 33217
author:
type: added
---
title: Filter potentially-sensitive Sidekiq arguments from logs and Sentry
merge_request: 33967
author:
type: changed
# frozen_string_literal: true
class CreateAlertManagementAlertUserMentions < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :alert_management_alert_user_mentions do |t|
t.references :alert_management_alert, type: :bigint, index: false, null: false, foreign_key: { on_delete: :cascade }
t.bigint :note_id, null: true
t.integer :mentioned_users_ids, array: true
t.integer :mentioned_projects_ids, array: true
t.integer :mentioned_groups_ids, array: true
end
add_index :alert_management_alert_user_mentions, [:note_id], where: 'note_id IS NOT NULL', unique: true, name: 'index_alert_user_mentions_on_note_id'
add_index :alert_management_alert_user_mentions, [:alert_management_alert_id], where: 'note_id IS NULL', unique: true, name: 'index_alert_user_mentions_on_alert_id'
add_index :alert_management_alert_user_mentions, [:alert_management_alert_id, :note_id], unique: true, name: 'index_alert_user_mentions_on_alert_id_and_note_id'
end
end
# frozen_string_literal: true
class AddForeignKeyToAlertManagementAlertUserMentions < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key :alert_management_alert_user_mentions, :notes, column: :note_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key :alert_management_alert_user_mentions, column: :note_id
end
end
end
......@@ -37,6 +37,24 @@ CREATE SEQUENCE public.alert_management_alert_assignees_id_seq
ALTER SEQUENCE public.alert_management_alert_assignees_id_seq OWNED BY public.alert_management_alert_assignees.id;
CREATE TABLE public.alert_management_alert_user_mentions (
id bigint NOT NULL,
alert_management_alert_id bigint NOT NULL,
note_id bigint,
mentioned_users_ids integer[],
mentioned_projects_ids integer[],
mentioned_groups_ids integer[]
);
CREATE SEQUENCE public.alert_management_alert_user_mentions_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.alert_management_alert_user_mentions_id_seq OWNED BY public.alert_management_alert_user_mentions.id;
CREATE TABLE public.alert_management_alerts (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
......@@ -7401,6 +7419,8 @@ ALTER TABLE ONLY public.abuse_reports ALTER COLUMN id SET DEFAULT nextval('publi
ALTER TABLE ONLY public.alert_management_alert_assignees ALTER COLUMN id SET DEFAULT nextval('public.alert_management_alert_assignees_id_seq'::regclass);
ALTER TABLE ONLY public.alert_management_alert_user_mentions ALTER COLUMN id SET DEFAULT nextval('public.alert_management_alert_user_mentions_id_seq'::regclass);
ALTER TABLE ONLY public.alert_management_alerts ALTER COLUMN id SET DEFAULT nextval('public.alert_management_alerts_id_seq'::regclass);
ALTER TABLE ONLY public.alerts_service_data ALTER COLUMN id SET DEFAULT nextval('public.alerts_service_data_id_seq'::regclass);
......@@ -8045,6 +8065,9 @@ ALTER TABLE ONLY public.abuse_reports
ALTER TABLE ONLY public.alert_management_alert_assignees
ADD CONSTRAINT alert_management_alert_assignees_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.alert_management_alert_user_mentions
ADD CONSTRAINT alert_management_alert_user_mentions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.alert_management_alerts
ADD CONSTRAINT alert_management_alerts_pkey PRIMARY KEY (id);
......@@ -9194,6 +9217,12 @@ CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_fingerprint
CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_iid ON public.alert_management_alerts USING btree (project_id, iid);
CREATE UNIQUE INDEX index_alert_user_mentions_on_alert_id ON public.alert_management_alert_user_mentions USING btree (alert_management_alert_id) WHERE (note_id IS NULL);
CREATE UNIQUE INDEX index_alert_user_mentions_on_alert_id_and_note_id ON public.alert_management_alert_user_mentions USING btree (alert_management_alert_id, note_id);
CREATE UNIQUE INDEX index_alert_user_mentions_on_note_id ON public.alert_management_alert_user_mentions USING btree (note_id) WHERE (note_id IS NOT NULL);
CREATE INDEX index_alerts_service_data_on_service_id ON public.alerts_service_data USING btree (service_id);
CREATE INDEX index_allowed_email_domains_on_group_id ON public.allowed_email_domains USING btree (group_id);
......@@ -12391,6 +12420,9 @@ ALTER TABLE ONLY public.design_user_mentions
ALTER TABLE ONLY public.clusters_kubernetes_namespaces
ADD CONSTRAINT fk_rails_8df789f3ab FOREIGN KEY (environment_id) REFERENCES public.environments(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.alert_management_alert_user_mentions
ADD CONSTRAINT fk_rails_8e48eca0fe FOREIGN KEY (alert_management_alert_id) REFERENCES public.alert_management_alerts(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.project_daily_statistics
ADD CONSTRAINT fk_rails_8e549b272d FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
......@@ -12769,6 +12801,9 @@ ALTER TABLE ONLY public.merge_request_blocks
ALTER TABLE ONLY public.protected_branch_unprotect_access_levels
ADD CONSTRAINT fk_rails_e9eb8dc025 FOREIGN KEY (protected_branch_id) REFERENCES public.protected_branches(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.alert_management_alert_user_mentions
ADD CONSTRAINT fk_rails_eb2de0cdef FOREIGN KEY (note_id) REFERENCES public.notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.ci_daily_report_results
ADD CONSTRAINT fk_rails_ebc2931b90 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
......@@ -13873,6 +13908,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200527151413
20200527152116
20200527152657
20200527170649
20200527211000
20200528054112
20200528123703
......@@ -13886,6 +13922,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200604145731
20200604174544
20200604174558
20200605003204
20200608072931
20200608075553
20200609002841
......
......@@ -29,9 +29,18 @@ Example:
gitlab_rails['env'] = {"SIDEKIQ_LOG_ARGUMENTS" => "1"}
```
Please note: It is not recommend to enable this setting in production because some
Sidekiq jobs (such as sending a password reset email) take secret arguments (for
example the password reset token).
This does not log all job arguments. To avoid logging sensitive
information (for instance, password reset tokens), it logs numeric
arguments for all workers, with overrides for some specific workers
where their arguments are not sensitive.
Example log output:
```json
{"severity":"INFO","time":"2020-06-08T14:37:37.892Z","class":"AdminEmailsWorker","args":["[FILTERED]","[FILTERED]","[FILTERED]"],"retry":3,"queue":"admin_emails","backtrace":true,"jid":"9e35e2674ac7b12d123e13cc","created_at":"2020-06-08T14:37:37.373Z","meta.user":"root","meta.caller_id":"Admin::EmailsController#create","correlation_id":"37D3lArJmT1","uber-trace-id":"2d942cc98cc1b561:6dc94409cfdd4d77:9fbe19bdee865293:1","enqueued_at":"2020-06-08T14:37:37.410Z","pid":65011,"message":"AdminEmailsWorker JID-9e35e2674ac7b12d123e13cc: done: 0.48085 sec","job_status":"done","scheduling_latency_s":0.001012,"redis_calls":9,"redis_duration_s":0.004608,"redis_read_bytes":696,"redis_write_bytes":6141,"duration_s":0.48085,"cpu_s":0.308849,"completed_at":"2020-06-08T14:37:37.892Z","db_duration_s":0.010742}
{"severity":"INFO","time":"2020-06-08T14:37:37.894Z","class":"ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper","wrapped":"ActionMailer::DeliveryJob","queue":"mailers","args":["[FILTERED]"],"retry":3,"backtrace":true,"jid":"e47a4f6793d475378432e3c8","created_at":"2020-06-08T14:37:37.884Z","meta.user":"root","meta.caller_id":"AdminEmailsWorker","correlation_id":"37D3lArJmT1","uber-trace-id":"2d942cc98cc1b561:29344de0f966446d:5c3b0e0e1bef987b:1","enqueued_at":"2020-06-08T14:37:37.885Z","pid":65011,"message":"ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper JID-e47a4f6793d475378432e3c8: start","job_status":"start","scheduling_latency_s":0.009473}
{"severity":"INFO","time":"2020-06-08T14:39:50.648Z","class":"NewIssueWorker","args":["455","1"],"retry":3,"queue":"new_issue","backtrace":true,"jid":"a24af71f96fd129ec47f5d1e","created_at":"2020-06-08T14:39:50.643Z","meta.user":"root","meta.project":"h5bp/html5-boilerplate","meta.root_namespace":"h5bp","meta.caller_id":"Projects::IssuesController#create","correlation_id":"f9UCZHqhuP7","uber-trace-id":"28f65730f99f55a3:a5d2b62dec38dffc:48ddd092707fa1b7:1","enqueued_at":"2020-06-08T14:39:50.646Z","pid":65011,"message":"NewIssueWorker JID-a24af71f96fd129ec47f5d1e: start","job_status":"start","scheduling_latency_s":0.001144}
```
When using [Sidekiq JSON logging](../logs.md#sidekiqlog),
arguments logs are limited to a maximum size of 10 kilobytes of text;
......
......@@ -168,7 +168,7 @@ type AdminSidekiqQueuesDeleteJobsPayload {
"""
Describes an alert from the project's Alert Management
"""
type AlertManagementAlert {
type AlertManagementAlert implements Noteable {
"""
Assignees of the alert
"""
......@@ -209,6 +209,31 @@ type AlertManagementAlert {
"""
details: JSON
"""
All discussions on this noteable
"""
discussions(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): DiscussionConnection!
"""
Timestamp the alert ended
"""
......@@ -239,6 +264,31 @@ type AlertManagementAlert {
"""
monitoringTool: String
"""
All notes on this noteable
"""
notes(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): NoteConnection!
"""
Service the alert came from
"""
......@@ -11763,11 +11813,6 @@ type TestReport {
"""
id: ID!
"""
Pipeline that created the test report
"""
pipeline: Pipeline
"""
State of the test report
"""
......
......@@ -577,6 +577,63 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "discussions",
"description": "All discussions on this noteable",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DiscussionConnection",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "endedAt",
"description": "Timestamp the alert ended",
......@@ -673,6 +730,63 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "notes",
"description": "All notes on this noteable",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "NoteConnection",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "service",
"description": "Service the alert came from",
......@@ -760,7 +874,11 @@
],
"inputFields": null,
"interfaces": [
{
"kind": "INTERFACE",
"name": "Noteable",
"ofType": null
}
],
"enumValues": null,
"possibleTypes": null
......@@ -23585,6 +23703,11 @@
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{
"kind": "OBJECT",
"name": "AlertManagementAlert",
"ofType": null
},
{
"kind": "OBJECT",
"name": "Design",
......@@ -34805,20 +34928,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pipeline",
"description": "Pipeline that created the test report",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Pipeline",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "state",
"description": "State of the test report",
......@@ -1745,7 +1745,6 @@ Represents a requirement test report.
| `author` | User | Author of the test report |
| `createdAt` | Time! | Timestamp of when the test report was created |
| `id` | ID! | ID of the test report |
| `pipeline` | Pipeline | Pipeline that created the test report |
| `state` | TestReportState! | State of the test report |
## Timelog
......
......@@ -518,6 +518,34 @@ job needs to be scheduled with.
The `context_proc` which needs to return a hash with the context
information for the job.
## Arguments logging
When [`SIDEKIQ_LOG_ARGUMENTS`](../administration/troubleshooting/sidekiq.md#log-arguments-to-sidekiq-jobs)
is enabled, Sidekiq job arguments will be logged.
By default, the only arguments logged are numeric arguments, because
arguments of other types could contain sensitive information. To
override this, use `loggable_arguments` inside a worker with the indexes
of the arguments to be logged. (Numeric arguments do not need to be
specified here.)
For example:
```ruby
class MyWorker
include ApplicationWorker
loggable_arguments 1, 3
# object_id will be logged as it's numeric
# string_a will be logged due to the loggable_arguments call
# string_b will be filtered from logs
# string_c will be logged due to the loggable_arguments call
def perform(object_id, string_a, string_b, string_c)
end
end
```
## Tests
Each Sidekiq worker must be tested using RSpec, just like any other class. These
......
......@@ -26,10 +26,13 @@ module Gitlab
# Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
config.processors << ::Gitlab::ErrorTracking::Processor::SidekiqProcessor
# Sanitize authentication headers
config.sanitize_http_headers = %w[Authorization Private-Token]
config.tags = { program: Gitlab.process_name }
config.before_send = method(:before_send)
yield config if block_given?
end
end
......
# frozen_string_literal: true
require 'set'
module Gitlab
module ErrorTracking
module Processor
class SidekiqProcessor < ::Raven::Processor
FILTERED_STRING = '[FILTERED]'
def self.filter_arguments(args, klass)
args.lazy.with_index.map do |arg, i|
case arg
when Numeric
arg
else
if permitted_arguments_for_worker(klass).include?(i)
arg
else
FILTERED_STRING
end
end
end
end
def self.permitted_arguments_for_worker(klass)
@permitted_arguments_for_worker ||= {}
@permitted_arguments_for_worker[klass] ||=
begin
klass.constantize&.loggable_arguments&.to_set
rescue
Set.new
end
end
def self.loggable_arguments(args, klass)
Gitlab::Utils::LogLimitedArray
.log_limited_array(filter_arguments(args, klass))
.map(&:to_s)
.to_a
end
def process(value, key = nil)
sidekiq = value.dig(:extra, :sidekiq)
return value unless sidekiq
sidekiq = sidekiq.deep_dup
sidekiq.delete(:jobstr)
# 'args' in this hash => from Gitlab::ErrorTracking.track_*
# 'args' in :job => from default error handler
job_holder = sidekiq.key?('args') ? sidekiq : sidekiq[:job]
if job_holder['args']
job_holder['args'] = self.class.filter_arguments(job_holder['args'], job_holder['class']).to_a
end
value[:extra][:sidekiq] = sidekiq
value
end
end
end
end
end
......@@ -14,18 +14,11 @@ module Gitlab
payload.delete('extra.server')
# The raven extra context is populated by Raven::SidekiqCleanupMiddleware.
#
# It contains the full sidekiq job which consists of mixed types and nested
# objects. That causes a bunch of issues when trying to ingest logs into
# Elasticsearch.
#
# We apply a stricter schema here that forces the args to be an array of
# strings. This same logic exists in Gitlab::SidekiqLogging::JSONFormatter.
payload['extra.sidekiq'].tap do |value|
if value.is_a?(Hash) && value.key?('args')
value = value.dup
payload['extra.sidekiq']['args'] = Gitlab::Utils::LogLimitedArray.log_limited_array(value['args'].try(:map, &:to_s))
payload['extra.sidekiq']['args'] = Gitlab::ErrorTracking::Processor::SidekiqProcessor
.loggable_arguments(value['args'], value['class'])
end
end
......
......@@ -18,10 +18,15 @@ module Gitlab
when String
output[:message] = data
when Hash
convert_to_iso8601!(data)
convert_retry_to_integer!(data)
stringify_args!(data)
output.merge!(data)
# jobstr is redundant and can include information we wanted to
# exclude (like arguments)
output.delete(:jobstr)
convert_to_iso8601!(output)
convert_retry_to_integer!(output)
process_args!(output)
end
output.to_json + "\n"
......@@ -56,8 +61,11 @@ module Gitlab
end
end
def stringify_args!(payload)
payload['args'] = Gitlab::Utils::LogLimitedArray.log_limited_array(payload['args'].map(&:to_s)) if payload['args']
def process_args!(payload)
return unless payload['args']
payload['args'] = Gitlab::ErrorTracking::Processor::SidekiqProcessor
.loggable_arguments(payload['args'], payload['class'])
end
end
end
......
......@@ -9,14 +9,14 @@ module Gitlab
# to around 10 kilobytes. Once we hit the limit, add the sentinel
# value as the last item in the returned array.
def self.log_limited_array(array, sentinel: '...')
return [] unless array.is_a?(Array)
return [] unless array.is_a?(Array) || array.is_a?(Enumerator::Lazy)
total_length = 0
limited_array = array.take_while do |arg|
total_length += JsonSizeEstimator.estimate(arg)
total_length <= MAXIMUM_ARRAY_LENGTH
end
end.to_a
limited_array.push(sentinel) if total_length > MAXIMUM_ARRAY_LENGTH
......
......@@ -17,6 +17,7 @@ FactoryBot.define do
factory :note_on_project_snippet, traits: [:on_project_snippet]
factory :note_on_personal_snippet, traits: [:on_personal_snippet]
factory :note_on_design, traits: [:on_design]
factory :note_on_alert, traits: [:on_alert]
factory :system_note, traits: [:system]
factory :discussion_note, class: 'DiscussionNote'
......@@ -145,6 +146,10 @@ FactoryBot.define do
end
end
trait :on_alert do
noteable { association(:alert_management_alert, project: project) }
end
trait :resolved do
resolved_at { Time.now }
resolved_by { association(:user) }
......
import { shallowMount } from '@vue/test-utils';
import SystemNote from '~/alert_management/components/system_notes/system_note.vue';
import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[1];
describe('Alert Details System Note', () => {
let wrapper;
function mountComponent({ stubs = {} } = {}) {
wrapper = shallowMount(SystemNote, {
propsData: {
note: { ...mockAlert.notes.nodes[0] },
},
stubs,
});
}
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
describe('System notes', () => {
beforeEach(() => {
mountComponent({});
});
it('renders the correct system note', () => {
expect(wrapper.find('.note-wrapper').attributes('id')).toBe('note_1628');
});
});
});
......@@ -8,7 +8,8 @@
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "TRIGGERED",
"assignees": { "nodes": [] }
"assignees": { "nodes": [] },
"notes": { "nodes": [] }
},
{
"iid": "1527543",
......@@ -18,7 +19,23 @@
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "ACKNOWLEDGED",
"assignees": { "nodes": [{ "username": "root" }] }
"assignees": { "nodes": [{ "username": "root" }] },
"notes": {
"nodes": [
{
"id": "gid://gitlab/Note/1628",
"author": {
"id": "gid://gitlab/User/1",
"state": "active",
"__typename": "User",
"avatarUrl": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"name": "Administrator",
"username": "root",
"webUrl": "http://192.168.1.4:3000/root"
}
}
]
}
},
{
"iid": "1527544",
......@@ -28,6 +45,22 @@
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "RESOLVED",
"assignees": { "nodes": [{ "username": "root" }] }
"assignees": { "nodes": [{ "username": "root" }] },
"notes": {
"nodes": [
{
"id": "gid://gitlab/Note/1629",
"author": {
"id": "gid://gitlab/User/2",
"state": "active",
"__typename": "User",
"avatarUrl": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"name": "Administrator",
"username": "root",
"webUrl": "http://192.168.1.4:3000/root"
}
}
]
}
}
]
......@@ -25,6 +25,8 @@ describe GitlabSchema.types['AlertManagementAlert'] do
created_at
updated_at
assignees
notes
discussions
]
expect(described_class).to have_graphql_fields(*expected_fields)
......
......@@ -17,6 +17,7 @@ describe Types::Notes::NoteableType do
expect(described_class.resolve_type(build(:issue), {})).to eq(Types::IssueType)
expect(described_class.resolve_type(build(:merge_request), {})).to eq(Types::MergeRequestType)
expect(described_class.resolve_type(build(:design), {})).to eq(Types::DesignManagement::DesignType)
expect(described_class.resolve_type(build(:alert_management_alert), {})).to eq(Types::AlertManagement::AlertType)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require 'rspec-parameterized'
RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor do
after do
if described_class.instance_variable_defined?(:@permitted_arguments_for_worker)
described_class.remove_instance_variable(:@permitted_arguments_for_worker)
end
end
describe '.filter_arguments' do
it 'returns a lazy enumerator' do
filtered = described_class.filter_arguments([1, 'string'], 'TestWorker')
expect(filtered).to be_a(Enumerator::Lazy)
expect(filtered.to_a).to eq([1, described_class::FILTERED_STRING])
end
context 'arguments filtering' do
using RSpec::Parameterized::TableSyntax
where(:klass, :expected) do
'UnknownWorker' | [1, described_class::FILTERED_STRING, described_class::FILTERED_STRING, described_class::FILTERED_STRING]
'NoPermittedArguments' | [1, described_class::FILTERED_STRING, described_class::FILTERED_STRING, described_class::FILTERED_STRING]
'OnePermittedArgument' | [1, 'string', described_class::FILTERED_STRING, described_class::FILTERED_STRING]
'AllPermittedArguments' | [1, 'string', [1, 2], { a: 1 }]
end
with_them do
before do
stub_const('NoPermittedArguments', double(loggable_arguments: []))
stub_const('OnePermittedArgument', double(loggable_arguments: [1]))
stub_const('AllPermittedArguments', double(loggable_arguments: [0, 1, 2, 3]))
end
it do
expect(described_class.filter_arguments([1, 'string', [1, 2], { a: 1 }], klass).to_a)
.to eq(expected)
end
end
end
end
describe '.permitted_arguments_for_worker' do
it 'returns the loggable_arguments for a worker class as a set' do
stub_const('TestWorker', double(loggable_arguments: [1, 1]))
expect(described_class.permitted_arguments_for_worker('TestWorker'))
.to eq([1].to_set)
end
it 'returns an empty set when the worker class does not exist' do
expect(described_class.permitted_arguments_for_worker('TestWorker'))
.to eq(Set.new)
end
it 'returns an empty set when the worker class does not respond to loggable_arguments' do
stub_const('TestWorker', 1)
expect(described_class.permitted_arguments_for_worker('TestWorker'))
.to eq(Set.new)
end
it 'returns an empty set when loggable_arguments cannot be converted to a set' do
stub_const('TestWorker', double(loggable_arguments: 1))
expect(described_class.permitted_arguments_for_worker('TestWorker'))
.to eq(Set.new)
end
it 'memoizes the results' do
worker_class = double
stub_const('TestWorker', worker_class)
expect(worker_class).to receive(:loggable_arguments).once.and_return([])
described_class.permitted_arguments_for_worker('TestWorker')
described_class.permitted_arguments_for_worker('TestWorker')
end
end
describe '.loggable_arguments' do
it 'filters and limits the arguments, then converts to strings' do
half_limit = Gitlab::Utils::LogLimitedArray::MAXIMUM_ARRAY_LENGTH / 2
args = [[1, 2], 'a' * half_limit, 'b' * half_limit, 'c' * half_limit, 'd']
stub_const('LoggableArguments', double(loggable_arguments: [0, 1, 3, 4]))
expect(described_class.loggable_arguments(args, 'LoggableArguments'))
.to eq(['[1, 2]', 'a' * half_limit, '[FILTERED]', '...'])
end
end
describe '#process' do
context 'when there is Sidekiq data' do
shared_examples 'Sidekiq arguments' do |args_in_job_hash: true|
let(:path) { [:extra, :sidekiq, args_in_job_hash ? :job : nil, 'args'].compact }
let(:args) { [1, 'string', { a: 1 }, [1, 2]] }
it 'only allows numeric arguments for an unknown worker' do
value = { 'args' => args, 'class' => 'UnknownWorker' }
value = { job: value } if args_in_job_hash
expect(subject.process(extra_sidekiq(value)).dig(*path))
.to eq([1, described_class::FILTERED_STRING, described_class::FILTERED_STRING, described_class::FILTERED_STRING])
end
it 'allows all argument types for a permitted worker' do
value = { 'args' => args, 'class' => 'PostReceive' }
value = { job: value } if args_in_job_hash
expect(subject.process(extra_sidekiq(value)).dig(*path))
.to eq(args)
end
end
context 'when processing via the default error handler' do
include_examples 'Sidekiq arguments', args_in_job_hash: true
end
context 'when processing via Gitlab::ErrorTracking' do
include_examples 'Sidekiq arguments', args_in_job_hash: false
end
it 'removes a jobstr field if present' do
value = {
job: { 'args' => [1] },
jobstr: { 'args' => [1] }.to_json
}
expect(subject.process(extra_sidekiq(value)))
.to eq(extra_sidekiq(value.except(:jobstr)))
end
it 'does nothing with no jobstr' do
value = { job: { 'args' => [1] } }
expect(subject.process(extra_sidekiq(value)))
.to eq(extra_sidekiq(value))
end
end
context 'when there is no Sidekiq data' do
it 'does nothing' do
value = {
request: {
method: 'POST',
data: { 'key' => 'value' }
}
}
expect(subject.process(value)).to eq(value)
end
end
def extra_sidekiq(hash)
{ extra: { sidekiq: hash } }
end
end
end
......@@ -2,6 +2,8 @@
require 'spec_helper'
require 'raven/transports/dummy'
describe Gitlab::ErrorTracking do
let(:exception) { RuntimeError.new('boom') }
let(:issue_url) { 'http://gitlab.com/gitlab-org/gitlab-foss/issues/1' }
......@@ -22,7 +24,9 @@ describe Gitlab::ErrorTracking do
allow(described_class).to receive(:sentry_dsn).and_return(Gitlab.config.sentry.dsn)
allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid')
described_class.configure
described_class.configure do |config|
config.encoding = 'json'
end
end
describe '.with_context' do
......@@ -181,13 +185,26 @@ describe Gitlab::ErrorTracking do
end
context 'with sidekiq args' do
let(:extra) { { sidekiq: { 'args' => [1, { 'id' => 2, 'name' => 'hello' }, 'some-value', 'another-value'] } } }
it 'ensures extra.sidekiq.args is a string' do
extra = { sidekiq: { 'class' => 'PostReceive', 'args' => [1, { 'id' => 2, 'name' => 'hello' }, 'some-value', 'another-value'] } }
expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(
hash_including({ 'extra.sidekiq' => { 'class' => 'PostReceive', 'args' => ['1', '{"id"=>2, "name"=>"hello"}', 'some-value', 'another-value'] } }))
described_class.track_exception(exception, extra)
end
it 'filters sensitive arguments before sending' do
extra = { sidekiq: { 'class' => 'UnknownWorker', 'args' => ['sensitive string', 1, 2] } }
expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(
hash_including({ 'extra.sidekiq' => { 'args' => ['1', '{"id"=>2, "name"=>"hello"}', 'some-value', 'another-value'] } }))
hash_including('extra.sidekiq' => { 'class' => 'UnknownWorker', 'args' => ['[FILTERED]', '1', '2'] }))
described_class.track_exception(exception, extra)
sentry_event = Gitlab::Json.parse(Raven.client.transport.events.last[1])
expect(sentry_event.dig('extra', 'sidekiq', 'args')).to eq(['[FILTERED]', 1, 2])
end
end
end
......
......@@ -14,6 +14,7 @@ describe Gitlab::SidekiqLogging::JSONFormatter do
let(:hash_input) do
{
foo: 1,
'class' => 'PostReceive',
'bar' => 'test',
'created_at' => timestamp,
'enqueued_at' => timestamp,
......@@ -42,21 +43,47 @@ describe Gitlab::SidekiqLogging::JSONFormatter do
expect(subject).to eq(expected_output)
end
context 'when the job args are bigger than the maximum allowed' do
it 'keeps args from the front until they exceed the limit' do
half_limit = Gitlab::Utils::LogLimitedArray::MAXIMUM_ARRAY_LENGTH / 2
hash_input['args'] = [1, 2, 'a' * half_limit, 'b' * half_limit, 3]
it 'removes jobstr from the hash' do
hash_input[:jobstr] = 'job string'
expected_args = hash_input['args'].take(3).map(&:to_s) + ['...']
expect(subject).not_to include('jobstr')
end
expect(subject['args']).to eq(expected_args)
end
it 'does not modify the input hash' do
input = { 'args' => [1, 'string'] }
output = Gitlab::Json.parse(described_class.new.call('INFO', now, 'my program', input))
expect(input['args']).to eq([1, 'string'])
expect(output['args']).to eq(['1', '[FILTERED]'])
end
it 'properly flattens arguments to a String' do
hash_input['args'] = [1, "test", 2, { 'test' => 1 }]
context 'job arguments' do
context 'when the arguments are bigger than the maximum allowed' do
it 'keeps args from the front until they exceed the limit' do
half_limit = Gitlab::Utils::LogLimitedArray::MAXIMUM_ARRAY_LENGTH / 2
hash_input['args'] = [1, 2, 'a' * half_limit, 'b' * half_limit, 3]
expected_args = hash_input['args'].take(3).map(&:to_s) + ['...']
expect(subject['args']).to eq(expected_args)
end
end
context 'when the job has non-integer arguments' do
it 'only allows permitted non-integer arguments through' do
hash_input['args'] = [1, 'foo', 'bar']
hash_input['class'] = 'WebHookWorker'
expect(subject['args']).to eq(["1", "test", "2", %({"test"=>1})])
expect(subject['args']).to eq(['1', '[FILTERED]', 'bar'])
end
end
it 'properly flattens arguments to a String' do
hash_input['args'] = [1, "test", 2, { 'test' => 1 }]
expect(subject['args']).to eq(["1", "test", "2", %({"test"=>1})])
end
end
context 'when the job has a non-integer value for retry' do
......
......@@ -7,6 +7,8 @@ describe AlertManagement::Alert do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:issue) }
it { is_expected.to have_many(:assignees).through(:alert_assignees) }
it { is_expected.to have_many(:notes) }
it { is_expected.to have_many(:user_mentions) }
end
describe 'validations' do
......
# frozen_string_literal: true
require 'spec_helper'
describe AlertManagement::AlertUserMention do
describe 'associations' do
it { is_expected.to belong_to(:alert_management_alert) }
it { is_expected.to belong_to(:note) }
end
it_behaves_like 'has user mentions'
end
......@@ -829,6 +829,10 @@ describe Note do
it 'returns commit for a commit note' do
expect(build(:note_on_commit).noteable_ability_name).to eq('commit')
end
it 'returns alert_management_alert for an alert note' do
expect(build(:note_on_alert).noteable_ability_name).to eq('alert_management_alert')
end
end
describe '#cache_markdown_field' do
......
......@@ -10,6 +10,8 @@ describe 'getting Alert Management Alerts' do
let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low) }
let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload) }
let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields) }
let_it_be(:system_note) { create(:note_on_alert, noteable: triggered_alert, project: project) }
let(:params) { {} }
let(:fields) do
......@@ -75,6 +77,8 @@ describe 'getting Alert Management Alerts' do
'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')
)
expect(first_alert['notes']['nodes'].first).to include('id' => system_note.to_global_id.to_s)
expect(second_alert).to include(
'iid' => resolved_alert.iid.to_s,
'issueIid' => nil,
......
......@@ -40,6 +40,7 @@ describe AlertManagement::Alerts::UpdateService do
let(:params) { { title: nil } }
it 'results in an error' do
expect { response }.not_to change { alert.reload.notes.count }
expect(response).to be_error
expect(response.message).to eq("Title can't be blank")
end
......@@ -72,6 +73,14 @@ describe AlertManagement::Alerts::UpdateService do
expect(response).to be_success
end
it 'creates a system note for the assignment' do
expect { response }.to change { alert.reload.notes.count }.by(1)
end
it 'adds a todo' do
expect { response }.to change { Todo.where(user: user_with_permissions).count }.by(1)
end
context 'with multiple users included' do
let(:params) { { assignees: [user_with_permissions, user_without_permissions] } }
......@@ -80,10 +89,6 @@ describe AlertManagement::Alerts::UpdateService do
expect(response).to be_success
end
end
it 'adds a todo' do
expect { response }.to change { Todo.where(user: user_with_permissions).count }.by(1)
end
end
end
end
......@@ -40,6 +40,12 @@ describe 'Every Sidekiq worker' do
end
end
it 'has a value for loggable_arguments' do
workers_without_defaults.each do |worker|
expect(worker.klass.loggable_arguments).to be_an(Array)
end
end
describe "feature category declarations" do
let(:feature_categories) do
YAML.load_file(Rails.root.join('config', 'feature_categories.yml')).map(&:to_sym).to_set
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册