Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-06-12 12:08:56 +00:00
parent 6be19afab7
commit 3ef9553486
100 changed files with 1041 additions and 112 deletions

View File

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.7 KiB

View 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"
/>

View File

@ -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)"
/>

View File

@ -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);

View File

@ -0,0 +1,39 @@
<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>

View File

@ -0,0 +1,16 @@
#import "~/graphql_shared/fragments/author.fragment.graphql"
fragment AlertNote on Note {
id
author {
id
state
...Author
}
body
bodyHtml
createdAt
discussion {
id
}
}

View File

@ -1,4 +1,5 @@
#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
}
}
}

View File

@ -1,4 +1,4 @@
#import "../fragments/detailItem.fragment.graphql"
#import "../fragments/detail_item.fragment.graphql"
query alertDetails($fullPath: ID!, $alertId: String) {
project(fullPath: $fullPath) {

View File

@ -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;
}

View File

@ -58,6 +58,42 @@
}
}
// Essentially were doing @include form-control-focus here (from
// bootstrap/scss/mixins/_forms.scss), except that the bootstrap mixin adds a
// `&:focus` selector and were 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 were not
// block-truncating the container.
&:not(.block-truncated) .select2-choice {
@include form-control-focus-glow;
}
}
&.is-invalid {
~ .invalid-feedback {
display: block;

View File

@ -0,0 +1,29 @@
.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;
}
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
module AlertManagement
class AlertUserMention < UserMention
belongs_to :alert_management_alert, class_name: '::AlertManagement::Alert'
belongs_to :note
end
end

View File

@ -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?

View File

@ -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

View File

@ -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) + '/' }

View File

@ -1,26 +1,28 @@
- page_title _('Whats 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= _('Im 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= _('Im 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= _('Im 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= _('Im 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'

View File

@ -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?

View File

@ -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.

View File

@ -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

View File

@ -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|

View File

@ -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|

View File

@ -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

View File

@ -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|

View File

@ -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|

View File

@ -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

View File

@ -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|

View File

@ -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

View File

@ -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

View File

@ -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|

View File

@ -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|

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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 = {

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -5,6 +5,7 @@ module HashedStorage
include ApplicationWorker
queue_namespace :hashed_storage
loggable_arguments 1
attr_reader :project_id

View File

@ -5,6 +5,7 @@ module HashedStorage
include ApplicationWorker
queue_namespace :hashed_storage
loggable_arguments 1
attr_reader :project_id

View File

@ -9,6 +9,7 @@ module MailScheduler
feature_category :issue_tracking
worker_resource_boundary :cpu
loggable_arguments 0
def perform(meth, *args)
check_arguments!(args)

View File

@ -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

View File

@ -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

View File

@ -7,6 +7,7 @@ module ObjectStorage
include ObjectStorageQueue
feature_category_not_owned!
loggable_arguments 0, 1, 2, 3
SanityCheckError = Class.new(StandardError)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -4,8 +4,8 @@ class PropagateIntegrationWorker
include ApplicationWorker
feature_category :integrations
idempotent!
loggable_arguments 1
def perform(integration_id, overwrite)
Admin::PropagateIntegrationService.propagate(

View File

@ -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)

View File

@ -12,6 +12,8 @@ module RepositoryCheck
attr_reader :shard_name
loggable_arguments 0
def perform(shard_name)
@shard_name = shard_name

View File

@ -5,6 +5,7 @@ class RepositoryRemoveRemoteWorker # rubocop:disable Scalability/IdempotentWorke
include ExclusiveLeaseGuard
feature_category :source_code_management
loggable_arguments 1
LEASE_TIMEOUT = 1.hour

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Fix tabbing through form fields in projects/new flow
merge_request: 33209
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add system note when assigning user to alert
merge_request: 33217
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Filter potentially-sensitive Sidekiq arguments from logs and Sentry
merge_request: 33967
author:
type: changed

View File

@ -0,0 +1,20 @@
# 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

View File

@ -0,0 +1,19 @@
# 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

View File

@ -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

View File

@ -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;

View File

@ -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
"""

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,66 @@
# 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -0,0 +1,34 @@
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');
});
});
});

View File

@ -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"
}
}
]
}
}
]

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,165 @@
# 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

View File

@ -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,14 +185,27 @@ 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' => { 'args' => ['1', '{"id"=>2, "name"=>"hello"}', 'some-value', 'another-value'] } }))
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' => { '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
end

View File

@ -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['args']).to eq(expected_args)
end
expect(subject).not_to include('jobstr')
end
it 'properly flattens arguments to a String' do
hash_input['args'] = [1, "test", 2, { 'test' => 1 }]
it 'does not modify the input hash' do
input = { 'args' => [1, 'string'] }
expect(subject['args']).to eq(["1", "test", "2", %({"test"=>1})])
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
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', '[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

View File

@ -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

View File

@ -0,0 +1,12 @@
# 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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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