Commit 3c5b180f authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'mwaw/219398-improve-validation-errors-for-metrics-dashboard-yaml' into 'master'

Improve metrics dashboard validation error messages

See merge request gitlab-org/gitlab!39123
parents ef23a1fa cc97f9ee
...@@ -34,7 +34,7 @@ module Gitlab ...@@ -34,7 +34,7 @@ module Gitlab
end end
def schemer def schemer
@schemer ||= JSONSchemer.schema(Pathname.new(schema_path), formats: custom_formats.format_handlers) @schemer ||= ::JSONSchemer.schema(Pathname.new(schema_path), formats: custom_formats.format_handlers)
end end
def validate_against_schema def validate_against_schema
......
...@@ -9,19 +9,42 @@ module Gitlab ...@@ -9,19 +9,42 @@ module Gitlab
class SchemaValidationError < InvalidDashboardError class SchemaValidationError < InvalidDashboardError
def initialize(error = {}) def initialize(error = {})
if error.is_a?(Hash) && error.present? super(error_message(error))
data = error["data"] end
data_pointer = error["data_pointer"]
schema = error["schema"]
schema_pointer = error["schema_pointer"]
msg = _("'%{data}' is invalid at '%{data_pointer}'. Should be '%{schema}' due to schema definition at '%{schema_pointer}'") % private
{ data: data, data_pointer: data_pointer, schema: schema, schema_pointer: schema_pointer }
def error_message(error)
if error.is_a?(Hash) && error.present?
pretty(error)
else else
msg = "Dashboard failed schema validation" "Dashboard failed schema validation"
end end
end
# based on https://github.com/davishmcclurg/json_schemer/blob/master/lib/json_schemer/errors.rb
# with addition ability to translate error messages
def pretty(error)
data, data_pointer, type, schema = error.values_at('data', 'data_pointer', 'type', 'schema')
location = data_pointer.empty? ? 'root' : data_pointer
super(msg) case type
when 'required'
keys = error.fetch('details').fetch('missing_keys').join(', ')
_("%{location} is missing required keys: %{keys}") % { location: location, keys: keys }
when 'null', 'string', 'boolean', 'integer', 'number', 'array', 'object'
_("'%{data}' at %{location} is not of type: %{type}") % { data: data, location: location, type: type }
when 'pattern'
_("'%{data}' at %{location} does not match pattern: %{pattern}") % { data: data, location: location, pattern: schema.fetch('pattern') }
when 'format'
_("'%{data}' at %{location} does not match format: %{format}") % { data: data, location: location, format: schema.fetch('format') }
when 'const'
_("'%{data}' at %{location} is not: %{const}") % { data: data, location: location, const: schema.fetch('const').inspect }
when 'enum'
_("'%{data}' at %{location} is not one of: %{enum}") % { data: data, location: location, enum: schema.fetch('enum') }
else
_("'%{data}' at %{location} is invalid: error_type=%{type}") % { data: data, location: location, type: type }
end
end end
end end
......
...@@ -527,6 +527,9 @@ msgstr "" ...@@ -527,6 +527,9 @@ msgstr ""
msgid "%{loadingIcon} Started" msgid "%{loadingIcon} Started"
msgstr "" msgstr ""
msgid "%{location} is missing required keys: %{keys}"
msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}" msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr "" msgstr ""
...@@ -806,7 +809,22 @@ msgstr "" ...@@ -806,7 +809,22 @@ msgstr ""
msgid "&lt;project name&gt;" msgid "&lt;project name&gt;"
msgstr "" msgstr ""
msgid "'%{data}' is invalid at '%{data_pointer}'. Should be '%{schema}' due to schema definition at '%{schema_pointer}'" msgid "'%{data}' at %{location} does not match format: %{format}"
msgstr ""
msgid "'%{data}' at %{location} does not match pattern: %{pattern}"
msgstr ""
msgid "'%{data}' at %{location} is invalid: error_type=%{type}"
msgstr ""
msgid "'%{data}' at %{location} is not of type: %{type}"
msgstr ""
msgid "'%{data}' at %{location} is not one of: %{enum}"
msgstr ""
msgid "'%{data}' at %{location} is not: %{const}"
msgstr "" msgstr ""
msgid "'%{level}' is not a valid visibility level" msgid "'%{level}' is not a valid visibility level"
......
dashboard: 'Test Dashboard'
panel_groups:
- panels:
- title: "Super Chart A1"
type: "area-chart"
y_label: "y_label"
weight: 1
max_value: 1
metrics:
- id: metric_a1
query_range: |+
avg(
sum(
container_memory_usage_bytes{
container_name!="POD",
pod_name=~"^{{ci_environment_slug}}-(.*)",
namespace="{{kube_namespace}}"
}
) by (job)
) without (job)
/1024/1024/1024
unit: unit
label: Legend Label
- title: "Super Chart A2"
type: "area-chart"
y_label: "y_label"
weight: 2
metrics:
- id: metric_a2
query_range: 'query'
label: Legend Label
unit: unit
- group: Group B
---
- dashboard: 'Test Dashboard'
panel_groups:
- group: Group A
panels:
- title: "Super Chart A2"
type: "area-chart"
y_label: "y_label"
weight: 2
metrics:
- id: metric_a2
query_range: 'query'
label: Legend Label
unit: unit
- dashboard: 'second entry'
dashboard: 'Test Dashboard'
priority: 1
links:
- title: Link 1
url: https://gitlab.com
- title: Link 2
url: https://docs.gitlab.com
templating:
variables:
text_variable_full_syntax:
label: 'Variable 1'
type: text
options:
default_value: 'default'
text_variable_simple_syntax: 'default value'
custom_variable_simple_syntax: ['value1', 'value2', 'value3']
custom_variable_full_syntax:
label: 'Variable 2'
type: custom
options:
values:
- value: 'value option 1'
text: 'Option 1'
- value: 'value_option_2'
text: 'Option 2'
default: true
metric_label_values_variable:
label: 'Variable 3'
type: metric_label_values
options:
series_selector: 'backend:haproxy_backend_availability:ratio{env="{{env}}"}'
label: 'backend'
dashboard: 'Test Dashboard'
panel_groups:
- group: Group A
priority: 1
panels:
- title: "Super Chart A1"
type: "area-chart"
y_label: "y_label"
weight: 1
max_value: 1
- title: "Super Chart A2"
type: "area-chart"
y_label: "y_label"
weight: 2
metrics:
dashboard: 'Test Dashboard'
priority: 1
links:
- title: Link 1
url: https://gitlab.com
- title: Link 2
url: https://docs.gitlab.com
templating:
variables:
text_variable_full_syntax:
label: 'Variable 1'
type: text
options:
default_value: 'default'
text_variable_simple_syntax: 'default value'
custom_variable_simple_syntax: ['value1', 'value2', 'value3']
custom_variable_full_syntax:
label: 'Variable 2'
type: custom
options:
values:
- value: 'value option 1'
text: 'Option 1'
- value: 'value_option_2'
text: 'Option 2'
default: true
metric_label_values_variable:
label: 'Variable 3'
type: metric_label_values
options:
series_selector: 'backend:haproxy_backend_availability:ratio{env="{{env}}"}'
label: 'backend'
panel_groups: this should be an array
...@@ -4,28 +4,130 @@ require 'spec_helper' ...@@ -4,28 +4,130 @@ require 'spec_helper'
RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do
describe Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError do describe Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError do
context 'valid error hash from jsonschemer' do context 'empty error hash' do
let(:error_hash) { {} }
it 'uses default error message' do
expect(described_class.new(error_hash).message).to eq('Dashboard failed schema validation')
end
end
context 'formatted message' do
subject { described_class.new(error_hash).message }
let(:error_hash) do let(:error_hash) do
{ {
'data' => 'data', 'data' => 'property_name',
'data_pointer' => 'data_pointer', 'data_pointer' => pointer,
'schema' => 'schema', 'type' => type,
'schema_pointer' => 'schema_pointer' 'schema' => 'schema',
'details' => details
} }
end end
it 'formats message' do context 'for root object' do
expect(described_class.new(error_hash).message).to eq( let(:pointer) { '' }
"'data' is invalid at 'data_pointer'. Should be 'schema' due to schema definition at 'schema_pointer'"
) context 'when required keys are missing' do
let(:type) { 'required' }
let(:details) { { 'missing_keys' => ['one'] } }
it { is_expected.to eq 'root is missing required keys: one' }
end
end end
end
context 'empty error hash' do context 'for nested object' do
let(:error_hash) { {} } let(:pointer) { '/nested_objects/0' }
it 'uses default error message' do context 'when required keys are missing' do
expect(described_class.new(error_hash).message).to eq('Dashboard failed schema validation') let(:type) { 'required' }
let(:details) { { 'missing_keys' => ['two'] } }
it { is_expected.to eq '/nested_objects/0 is missing required keys: two' }
end
context 'when there is type mismatch' do
%w(null string boolean integer number array object).each do |expected_type|
context "on type: #{expected_type}" do
let(:type) { expected_type }
let(:details) { nil }
subject { described_class.new(error_hash).message }
it { is_expected.to eq "'property_name' at /nested_objects/0 is not of type: #{expected_type}" }
end
end
end
context 'when data does not match pattern' do
let(:type) { 'pattern' }
let(:error_hash) do
{
'data' => 'property_name',
'data_pointer' => pointer,
'type' => type,
'schema' => { 'pattern' => 'aa.*' }
}
end
it { is_expected.to eq "'property_name' at /nested_objects/0 does not match pattern: aa.*" }
end
context 'when data does not match format' do
let(:type) { 'format' }
let(:error_hash) do
{
'data' => 'property_name',
'data_pointer' => pointer,
'type' => type,
'schema' => { 'format' => 'date-time' }
}
end
it { is_expected.to eq "'property_name' at /nested_objects/0 does not match format: date-time" }
end
context 'when data is not const' do
let(:type) { 'const' }
let(:error_hash) do
{
'data' => 'property_name',
'data_pointer' => pointer,
'type' => type,
'schema' => { 'const' => 'one' }
}
end
it { is_expected.to eq "'property_name' at /nested_objects/0 is not: \"one\"" }
end
context 'when data is not included in enum' do
let(:type) { 'enum' }
let(:error_hash) do
{
'data' => 'property_name',
'data_pointer' => pointer,
'type' => type,
'schema' => { 'enum' => %w(one two) }
}
end
it { is_expected.to eq "'property_name' at /nested_objects/0 is not one of: [\"one\", \"two\"]" }
end
context 'when data is not included in enum' do
let(:type) { 'unknown' }
let(:error_hash) do
{
'data' => 'property_name',
'data_pointer' => pointer,
'type' => type,
'schema' => 'schema'
}
end
it { is_expected.to eq "'property_name' at /nested_objects/0 is invalid: error_type=unknown" }
end
end end
end end
end end
......
...@@ -34,6 +34,15 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do ...@@ -34,6 +34,15 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do
end end
describe '#validate!' do describe '#validate!' do
shared_examples 'validation failed' do |errors_message|
it 'raises error with corresponding messages', :aggregate_failures do
expect { subject }.to raise_error do |error|
expect(error).to be_kind_of(Gitlab::Metrics::Dashboard::Validator::Errors::InvalidDashboardError)
expect(error.message).to eq(errors_message)
end
end
end
context 'valid dashboard' do context 'valid dashboard' do
it 'returns true' do it 'returns true' do
expect(described_class.validate!(valid_dashboard)).to be true expect(described_class.validate!(valid_dashboard)).to be true
...@@ -41,22 +50,37 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do ...@@ -41,22 +50,37 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do
end end
context 'invalid dashboard' do context 'invalid dashboard' do
subject { described_class.validate!(invalid_dashboard) }
context 'invalid schema' do context 'invalid schema' do
it 'raises error' do context 'wrong property type' do
expect { described_class.validate!(invalid_dashboard) } it_behaves_like 'validation failed', "'this_should_be_a_int' at /panel_groups/0/panels/0/weight is not of type: number"
.to raise_error(Gitlab::Metrics::Dashboard::Validator::Errors::InvalidDashboardError, end
"'this_should_be_a_int' is invalid at '/panel_groups/0/panels/0/weight'."\
" Should be '{\"type\"=>\"number\"}' due to schema definition at '/properties/weight'") context 'panel groups missing' do
let_it_be(:invalid_dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) }
it_behaves_like 'validation failed', 'root is missing required keys: panel_groups'
end
context 'groups are missing panels and group keys' do
let_it_be(:invalid_dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/dashboard_groups_missing_panels_and_group.yml')) }
it_behaves_like 'validation failed', '/panel_groups/0 is missing required keys: group'
end
context 'panel is missing metrics key' do
let_it_be(:invalid_dashboard) { load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/dashboard_panel_is_missing_metrics.yml')) }
it_behaves_like 'validation failed', '/panel_groups/0/panels/0 is missing required keys: metrics'
end end
end end
context 'duplicate metric ids' do context 'duplicate metric ids' do
context 'with no project given' do context 'with no project given' do
it 'checks against given dashboard and returns false' do subject { described_class.validate!(duplicate_id_dashboard) }
expect { described_class.validate!(duplicate_id_dashboard) }
.to raise_error(Gitlab::Metrics::Dashboard::Validator::Errors::InvalidDashboardError, it_behaves_like 'validation failed', 'metric_id must be unique across a project'
"metric_id must be unique across a project")
end
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment