diff --git a/docs/_static/admin-api-swagger.yml b/docs/_static/admin-api-swagger.yml index 4ec9617d6..bdd24f90f 100644 --- a/docs/_static/admin-api-swagger.yml +++ b/docs/_static/admin-api-swagger.yml @@ -358,6 +358,14 @@ paths: required: false type: string description: A raw elasticsearch query string. + - name: hits_over_time_max_buckets + in: query + required: false + type: integer + minimum: 1 + maximum: 100 + default: 10 + description: The maximum number of top results to include in the hits_over_time chart data. Defaults to 10. responses: 200: description: Successful response diff --git a/docs/admin/api-swagger.yml b/docs/admin/api-swagger.yml index 4ec9617d6..bdd24f90f 100644 --- a/docs/admin/api-swagger.yml +++ b/docs/admin/api-swagger.yml @@ -358,6 +358,14 @@ paths: required: false type: string description: A raw elasticsearch query string. + - name: hits_over_time_max_buckets + in: query + required: false + type: integer + minimum: 1 + maximum: 100 + default: 10 + description: The maximum number of top results to include in the hits_over_time chart data. Defaults to 10. responses: 200: description: Successful response diff --git a/src/api-umbrella/web-app/actions/v1/analytics.lua b/src/api-umbrella/web-app/actions/v1/analytics.lua index 0b4d2d31a..af641e46f 100644 --- a/src/api-umbrella/web-app/actions/v1/analytics.lua +++ b/src/api-umbrella/web-app/actions/v1/analytics.lua @@ -172,7 +172,7 @@ function _M.drilldown(self) search:aggregate_by_drilldown(self.params["prefix"], drilldown_size) if self.params["format"] ~= "csv" then - search:aggregate_by_drilldown_over_time() + search:aggregate_by_drilldown_over_time(self.params["hits_over_time_max_buckets"]) end local raw_results = search:fetch_results() diff --git a/src/api-umbrella/web-app/models/analytics_search_opensearch.lua b/src/api-umbrella/web-app/models/analytics_search_opensearch.lua index c46b6dcd8..24ea8d8dc 100644 --- a/src/api-umbrella/web-app/models/analytics_search_opensearch.lua +++ b/src/api-umbrella/web-app/models/analytics_search_opensearch.lua @@ -563,7 +563,7 @@ function _M:aggregate_by_drilldown(prefix, size) end end -function _M:aggregate_by_drilldown_over_time() +function _M:aggregate_by_drilldown_over_time(hits_over_time_max_buckets) if not is_empty(self.errors) then return false end @@ -573,9 +573,21 @@ function _M:aggregate_by_drilldown_over_time() assert(self.drilldown_prefix) assert(self.drilldown_depth) + local ok = validation_ext.optional.tonumber.number(hits_over_time_max_buckets) + if not ok then + add_error(self.errors, "hits_over_time_max_buckets", "hits_over_time_max_buckets", t("is not a number")) + return false + end + + local size = tonumber(hits_over_time_max_buckets) or 10 + if size < 1 or size > 100 then + add_error(self.errors, "hits_over_time_max_buckets", "hits_over_time_max_buckets", t("must be between 1 and 100")) + return false + end + self.body["aggregations"]["top_path_hits_over_time"] = { terms = { - size = 10, + size = size, }, aggregations = { drilldown_over_time = { diff --git a/test/apis/v1/analytics/test_drilldown.rb b/test/apis/v1/analytics/test_drilldown.rb index 9d3368dea..d3e22919b 100644 --- a/test/apis/v1/analytics/test_drilldown.rb +++ b/test/apis/v1/analytics/test_drilldown.rb @@ -401,6 +401,94 @@ def test_all_results_top_10_for_chart ] }, data["hits_over_time"]["rows"][1]) end + def test_hits_over_time_max_buckets + 13.times do |i| + FactoryBot.create(:log_item, :request_host => "127.0.0.#{i + 1}", :request_at => Time.parse("2015-01-15T00:00:00Z").utc) + end + LogItem.refresh_indices! + + response = Typhoeus.get("https://127.0.0.1:9081/api-umbrella/v1/analytics/drilldown.json", http_options.deep_merge(admin_token).deep_merge({ + :params => { + :search => "", + :start_at => "2015-01-13", + :end_at => "2015-01-18", + :interval => "day", + :prefix => "0/", + :hits_over_time_max_buckets => 13, + }, + })) + + assert_response_code(200, response) + data = MultiJson.load(response.body) + assert_equal(13, data["results"].length) + + # With hits_over_time_max_buckets=13, all 13 hosts should appear in the + # chart columns (date + 13 hosts = 14 columns, no "Other" column). + assert_equal(14, data["hits_over_time"]["cols"].length) + assert_equal({ "id" => "date", "label" => "Date", "type" => "datetime" }, data["hits_over_time"]["cols"][0]) + + # Verify the data row has 14 cells (date + 13 hosts, no "Other"). + assert_equal(14, data["hits_over_time"]["rows"][1]["c"].length) + end + + def test_hits_over_time_max_buckets_out_of_range + FactoryBot.create(:log_item, :request_at => Time.parse("2015-01-15T00:00:00Z").utc) + LogItem.refresh_indices! + + # Value over 100 should return a validation error. + response = Typhoeus.get("https://127.0.0.1:9081/api-umbrella/v1/analytics/drilldown.json", http_options.deep_merge(admin_token).deep_merge({ + :params => { + :search => "", + :start_at => "2015-01-13", + :end_at => "2015-01-18", + :interval => "day", + :prefix => "0/", + :hits_over_time_max_buckets => 101, + }, + })) + + assert_response_code(422, response) + + # Value of 0 should return a validation error. + response = Typhoeus.get("https://127.0.0.1:9081/api-umbrella/v1/analytics/drilldown.json", http_options.deep_merge(admin_token).deep_merge({ + :params => { + :search => "", + :start_at => "2015-01-13", + :end_at => "2015-01-18", + :interval => "day", + :prefix => "0/", + :hits_over_time_max_buckets => 0, + }, + })) + + assert_response_code(422, response) + end + + def test_hits_over_time_max_buckets_defaults_to_10 + 13.times do |i| + FactoryBot.create(:log_item, :request_host => "127.0.0.#{i + 1}", :request_at => Time.parse("2015-01-15T00:00:00Z").utc) + end + LogItem.refresh_indices! + + # Without hits_over_time_max_buckets, the chart should default to top 10 + # (date + 10 hosts + "Other" = 12 columns). + response = Typhoeus.get("https://127.0.0.1:9081/api-umbrella/v1/analytics/drilldown.json", http_options.deep_merge(admin_token).deep_merge({ + :params => { + :search => "", + :start_at => "2015-01-13", + :end_at => "2015-01-18", + :interval => "day", + :prefix => "0/", + }, + })) + + assert_response_code(200, response) + data = MultiJson.load(response.body) + assert_equal(13, data["results"].length) + assert_equal(12, data["hits_over_time"]["cols"].length) + assert_equal({ "id" => "other", "label" => "Other", "type" => "number" }, data["hits_over_time"]["cols"][11]) + end + def test_time_zone Time.use_zone("America/Denver") do FactoryBot.create(:log_item, :request_at => Time.zone.parse("2015-01-12T23:59:59"))