Commit 05f142ea authored by Imre Farkas's avatar Imre Farkas

Merge branch '37986-log_explorer_time_filtering' into 'master'

Add ability to filter by time for k8s log explorer

Closes #37986

See merge request gitlab-org/gitlab!22734
parents 46673705 7b48c627
...@@ -38,7 +38,7 @@ module Projects ...@@ -38,7 +38,7 @@ module Projects
end end
def filter_params def filter_params
params.permit(:container_name, :pod_name, :search) params.permit(:container_name, :pod_name, :search, :start, :end)
end end
def environment def environment
......
...@@ -29,7 +29,7 @@ module EE ...@@ -29,7 +29,7 @@ module EE
::Gitlab::Kubernetes::RolloutStatus.from_deployments(*deployments, pods: pods, legacy_deployments: legacy_deployments) ::Gitlab::Kubernetes::RolloutStatus.from_deployments(*deployments, pods: pods, legacy_deployments: legacy_deployments)
end end
def read_pod_logs(environment_id, pod_name, namespace, container: nil, search: nil) def read_pod_logs(environment_id, pod_name, namespace, container: nil, search: nil, start_time: nil, end_time: nil)
# environment_id is required for use in reactive_cache_updated(), # environment_id is required for use in reactive_cache_updated(),
# to invalidate the ETag cache. # to invalidate the ETag cache.
with_reactive_cache( with_reactive_cache(
...@@ -38,7 +38,9 @@ module EE ...@@ -38,7 +38,9 @@ module EE
'pod_name' => pod_name, 'pod_name' => pod_name,
'namespace' => namespace, 'namespace' => namespace,
'container' => container, 'container' => container,
'search' => search 'search' => search,
"start_time" => start_time,
"end_time" => end_time
) do |result| ) do |result|
result result
end end
...@@ -51,11 +53,13 @@ module EE ...@@ -51,11 +53,13 @@ module EE
pod_name = opts['pod_name'] pod_name = opts['pod_name']
namespace = opts['namespace'] namespace = opts['namespace']
search = opts['search'] search = opts['search']
start_time = opts['start_time']
end_time = opts['end_time']
handle_exceptions(_('Pod not found'), pod_name: pod_name, container_name: container, search: search) do handle_exceptions(_('Pod not found'), pod_name: pod_name, container_name: container, search: search, start_time: start_time, end_time: end_time) do
container ||= container_names_of(pod_name, namespace).first container ||= container_names_of(pod_name, namespace).first
pod_logs(pod_name, namespace, container: container, search: search) pod_logs(pod_name, namespace, container: container, search: search, start_time: start_time, end_time: end_time)
end end
end end
end end
...@@ -84,10 +88,10 @@ module EE ...@@ -84,10 +88,10 @@ module EE
private private
def pod_logs(pod_name, namespace, container: nil, search: nil) def pod_logs(pod_name, namespace, container: nil, search: nil, start_time: nil, end_time: nil)
enable_advanced_querying = ::Feature.enabled?(:enable_cluster_application_elastic_stack) && !!elastic_stack_client enable_advanced_querying = ::Feature.enabled?(:enable_cluster_application_elastic_stack) && !!elastic_stack_client
logs = if enable_advanced_querying logs = if enable_advanced_querying
elastic_stack_pod_logs(namespace, pod_name, container, search) elastic_stack_pod_logs(namespace, pod_name, container, search, start_time, end_time)
else else
platform_pod_logs(namespace, pod_name, container) platform_pod_logs(namespace, pod_name, container)
end end
...@@ -117,11 +121,11 @@ module EE ...@@ -117,11 +121,11 @@ module EE
end end
end end
def elastic_stack_pod_logs(namespace, pod_name, container_name, search) def elastic_stack_pod_logs(namespace, pod_name, container_name, search, start_time, end_time)
client = elastic_stack_client client = elastic_stack_client
return [] if client.nil? return [] if client.nil?
::Gitlab::Elasticsearch::Logs.new(client).pod_logs(namespace, pod_name, container_name, search) ::Gitlab::Elasticsearch::Logs.new(client).pod_logs(namespace, pod_name, container_name, search, start_time, end_time)
end end
def elastic_stack_client def elastic_stack_client
......
...@@ -7,7 +7,7 @@ class PodLogsService < ::BaseService ...@@ -7,7 +7,7 @@ class PodLogsService < ::BaseService
K8S_NAME_MAX_LENGTH = 253 K8S_NAME_MAX_LENGTH = 253
PARAMS = %w(pod_name container_name search).freeze PARAMS = %w(pod_name container_name search start end).freeze
SUCCESS_RETURN_KEYS = [:status, :logs, :pod_name, :container_name, :pods, :enable_advanced_querying].freeze SUCCESS_RETURN_KEYS = [:status, :logs, :pod_name, :container_name, :pods, :enable_advanced_querying].freeze
...@@ -15,6 +15,7 @@ class PodLogsService < ::BaseService ...@@ -15,6 +15,7 @@ class PodLogsService < ::BaseService
:check_deployment_platform, :check_deployment_platform,
:check_pod_names, :check_pod_names,
:check_pod_name, :check_pod_name,
:check_times,
:pod_logs, :pod_logs,
:filter_return_keys :filter_return_keys
...@@ -72,13 +73,24 @@ class PodLogsService < ::BaseService ...@@ -72,13 +73,24 @@ class PodLogsService < ::BaseService
success(result) success(result)
end end
def check_times(result)
Time.iso8601(params['start']) if params['start']
Time.iso8601(params['end']) if params['end']
success(result)
rescue ArgumentError
error(_('Invalid start or end time format'))
end
def pod_logs(result) def pod_logs(result)
response = environment.deployment_platform.read_pod_logs( response = environment.deployment_platform.read_pod_logs(
environment.id, environment.id,
result[:pod_name], result[:pod_name],
namespace, namespace,
container: result[:container_name], container: result[:container_name],
search: params['search'] search: params['search'],
start_time: params['start'],
end_time: params['end']
) )
return { status: :processing } unless response return { status: :processing } unless response
......
...@@ -10,51 +10,25 @@ module Gitlab ...@@ -10,51 +10,25 @@ module Gitlab
@client = client @client = client
end end
def pod_logs(namespace, pod_name, container_name = nil, search = nil) def pod_logs(namespace, pod_name, container_name = nil, search = nil, start_time = nil, end_time = nil)
query = { query = { bool: { must: [] } }.tap do |q|
bool: { filter_pod_name(q, pod_name)
must: [ filter_namespace(q, namespace)
{ filter_container_name(q, container_name)
match_phrase: { filter_search(q, search)
"kubernetes.pod.name" => { filter_times(q, start_time, end_time)
query: pod_name
}
}
},
{
match_phrase: {
"kubernetes.namespace" => {
query: namespace
}
}
}
]
}
}
# A pod can contain multiple containers.
# By default we return logs from every container
unless container_name.nil?
query[:bool][:must] << {
match_phrase: {
"kubernetes.container.name" => {
query: container_name
}
}
}
end end
unless search.nil? body = build_body(query)
query[:bool][:must] << { response = @client.search body: body
simple_query_string: {
query: search,
fields: [:message],
default_operator: :and
}
}
end
body = { format_response(response)
end
private
def build_body(query)
{
query: query, query: query,
# reverse order so we can query N-most recent records # reverse order so we can query N-most recent records
sort: [ sort: [
...@@ -66,8 +40,66 @@ module Gitlab ...@@ -66,8 +40,66 @@ module Gitlab
# fixed limit for now, we should support paginated queries # fixed limit for now, we should support paginated queries
size: ::Gitlab::Elasticsearch::Logs::LOGS_LIMIT size: ::Gitlab::Elasticsearch::Logs::LOGS_LIMIT
} }
end
response = @client.search body: body def filter_pod_name(query, pod_name)
query[:bool][:must] << {
match_phrase: {
"kubernetes.pod.name" => {
query: pod_name
}
}
}
end
def filter_namespace(query, namespace)
query[:bool][:must] << {
match_phrase: {
"kubernetes.namespace" => {
query: namespace
}
}
}
end
def filter_container_name(query, container_name)
# A pod can contain multiple containers.
# By default we return logs from every container
return if container_name.nil?
query[:bool][:must] << {
match_phrase: {
"kubernetes.container.name" => {
query: container_name
}
}
}
end
def filter_search(query, search)
return if search.nil?
query[:bool][:must] << {
simple_query_string: {
query: search,
fields: [:message],
default_operator: :and
}
}
end
def filter_times(query, start_time, end_time)
return unless start_time || end_time
time_range = { range: { :@timestamp => {} } }.tap do |tr|
tr[:range][:@timestamp][:gte] = start_time if start_time
tr[:range][:@timestamp][:lt] = end_time if end_time
end
query[:bool][:filter] = [time_range]
end
def format_response(response)
result = response.fetch("hits", {}).fetch("hits", []).map do |hit| result = response.fetch("hits", {}).fetch("hits", []).map do |hit|
{ {
timestamp: hit["_source"]["@timestamp"], timestamp: hit["_source"]["@timestamp"],
......
{
"query": {
"bool": {
"must": [
{
"match_phrase": {
"kubernetes.pod.name": {
"query": "production-6866bc8974-m4sk4"
}
}
},
{
"match_phrase": {
"kubernetes.namespace": {
"query": "autodevops-deploy-9-production"
}
}
}
],
"filter": [
{
"range": {
"@timestamp": {
"lt": "2019-12-13T14:35:34.034Z"
}
}
}
]
}
},
"sort": [
{
"@timestamp": {
"order": "desc"
}
},
{
"offset": {
"order": "desc"
}
}
],
"_source": [
"@timestamp",
"message"
],
"size": 500
}
{
"query": {
"bool": {
"must": [
{
"match_phrase": {
"kubernetes.pod.name": {
"query": "production-6866bc8974-m4sk4"
}
}
},
{
"match_phrase": {
"kubernetes.namespace": {
"query": "autodevops-deploy-9-production"
}
}
}
],
"filter": [
{
"range": {
"@timestamp": {
"gte": "2019-12-13T14:35:34.034Z"
}
}
}
]
}
},
"sort": [
{
"@timestamp": {
"order": "desc"
}
},
{
"offset": {
"order": "desc"
}
}
],
"_source": [
"@timestamp",
"message"
],
"size": 500
}
{
"query": {
"bool": {
"must": [
{
"match_phrase": {
"kubernetes.pod.name": {
"query": "production-6866bc8974-m4sk4"
}
}
},
{
"match_phrase": {
"kubernetes.namespace": {
"query": "autodevops-deploy-9-production"
}
}
}
],
"filter": [
{
"range": {
"@timestamp": {
"gte": "2019-12-13T14:35:34.034Z",
"lt": "2019-12-13T14:35:34.034Z"
}
}
}
]
}
},
"sort": [
{
"@timestamp": {
"order": "desc"
}
},
{
"offset": {
"order": "desc"
}
}
],
"_source": [
"@timestamp",
"message"
],
"size": 500
}
...@@ -18,10 +18,15 @@ describe Gitlab::Elasticsearch::Logs do ...@@ -18,10 +18,15 @@ describe Gitlab::Elasticsearch::Logs do
let(:pod_name) { "production-6866bc8974-m4sk4" } let(:pod_name) { "production-6866bc8974-m4sk4" }
let(:container_name) { "auto-deploy-app" } let(:container_name) { "auto-deploy-app" }
let(:search) { "foo +bar "} let(:search) { "foo +bar "}
let(:start_time) { "2019-12-13T14:35:34.034Z" }
let(:end_time) { "2019-12-13T14:35:34.034Z" }
let(:body) { JSON.parse(fixture_file('lib/elasticsearch/query.json', dir: 'ee')) } let(:body) { JSON.parse(fixture_file('lib/elasticsearch/query.json', dir: 'ee')) }
let(:body_with_container) { JSON.parse(fixture_file('lib/elasticsearch/query_with_container.json', dir: 'ee')) } let(:body_with_container) { JSON.parse(fixture_file('lib/elasticsearch/query_with_container.json', dir: 'ee')) }
let(:body_with_search) { JSON.parse(fixture_file('lib/elasticsearch/query_with_search.json', dir: 'ee')) } let(:body_with_search) { JSON.parse(fixture_file('lib/elasticsearch/query_with_search.json', dir: 'ee')) }
let(:body_with_times) { JSON.parse(fixture_file('lib/elasticsearch/query_with_times.json', dir: 'ee')) }
let(:body_with_start_time) { JSON.parse(fixture_file('lib/elasticsearch/query_with_start_time.json', dir: 'ee')) }
let(:body_with_end_time) { JSON.parse(fixture_file('lib/elasticsearch/query_with_end_time.json', dir: 'ee')) }
RSpec::Matchers.define :a_hash_equal_to_json do |expected| RSpec::Matchers.define :a_hash_equal_to_json do |expected|
match do |actual| match do |actual|
...@@ -50,5 +55,26 @@ describe Gitlab::Elasticsearch::Logs do ...@@ -50,5 +55,26 @@ describe Gitlab::Elasticsearch::Logs do
result = subject.pod_logs(namespace, pod_name, nil, search) result = subject.pod_logs(namespace, pod_name, nil, search)
expect(result).to eq([es_message_4, es_message_3, es_message_2, es_message_1]) expect(result).to eq([es_message_4, es_message_3, es_message_2, es_message_1])
end end
it 'can further filter the logs by start_time and end_time' do
expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_times)).and_return(es_response)
result = subject.pod_logs(namespace, pod_name, nil, nil, start_time, end_time)
expect(result).to eq([es_message_4, es_message_3, es_message_2, es_message_1])
end
it 'can further filter the logs by only start_time' do
expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_start_time)).and_return(es_response)
result = subject.pod_logs(namespace, pod_name, nil, nil, start_time)
expect(result).to eq([es_message_4, es_message_3, es_message_2, es_message_1])
end
it 'can further filter the logs by only end_time' do
expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_end_time)).and_return(es_response)
result = subject.pod_logs(namespace, pod_name, nil, nil, nil, end_time)
expect(result).to eq([es_message_4, es_message_3, es_message_2, es_message_1])
end
end end
end end
...@@ -268,7 +268,9 @@ describe Clusters::Platforms::Kubernetes do ...@@ -268,7 +268,9 @@ describe Clusters::Platforms::Kubernetes do
'pod_name' => pod_name, 'pod_name' => pod_name,
'namespace' => namespace, 'namespace' => namespace,
'container' => container, 'container' => container,
'search' => nil 'search' => nil,
'start_time' => nil,
'end_time' => nil
} }
] ]
end end
......
...@@ -17,13 +17,17 @@ describe PodLogsService do ...@@ -17,13 +17,17 @@ describe PodLogsService do
let(:enable_advanced_querying) { false } let(:enable_advanced_querying) { false }
let(:logs) { ['Log 1', 'Log 2', 'Log 3'] } let(:logs) { ['Log 1', 'Log 2', 'Log 3'] }
let(:result) { subject.execute } let(:result) { subject.execute }
let(:start_time) { nil }
let(:end_time) { nil }
let(:params) do let(:params) do
ActionController::Parameters.new( ActionController::Parameters.new(
{ {
'pod_name' => pod_name, 'pod_name' => pod_name,
'container_name' => container_name, 'container_name' => container_name,
'search' => search 'search' => search,
'start' => start_time,
'end' => end_time
} }
).permit! ).permit!
end end
...@@ -58,7 +62,7 @@ describe PodLogsService do ...@@ -58,7 +62,7 @@ describe PodLogsService do
shared_context 'return error' do |message| shared_context 'return error' do |message|
before do before do
allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs) allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs)
.with(environment.id, pod_name, environment.deployment_namespace, container: container_name, search: search) .with(environment.id, pod_name, environment.deployment_namespace, container: container_name, search: search, start_time: start_time, end_time: end_time)
.and_return({ .and_return({
status: :error, status: :error,
error: message, error: message,
...@@ -72,7 +76,7 @@ describe PodLogsService do ...@@ -72,7 +76,7 @@ describe PodLogsService do
shared_context 'return success' do shared_context 'return success' do
before do before do
allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs) allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs)
.with(environment.id, response_pod_name, environment.deployment_namespace, container: container_name, search: search) .with(environment.id, response_pod_name, environment.deployment_namespace, container: container_name, search: search, start_time: start_time, end_time: end_time)
.and_return({ .and_return({
status: :success, status: :success,
logs: ["Log 1", "Log 2", "Log 3"], logs: ["Log 1", "Log 2", "Log 3"],
...@@ -157,7 +161,7 @@ describe PodLogsService do ...@@ -157,7 +161,7 @@ describe PodLogsService do
it 'returns logs of first pod' do it 'returns logs of first pod' do
expect_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs) expect_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs)
.with(environment.id, first_pod_name, environment.deployment_namespace, container: nil, search: search) .with(environment.id, first_pod_name, environment.deployment_namespace, container: nil, search: search, start_time: start_time, end_time: end_time)
subject.execute subject.execute
end end
...@@ -193,6 +197,26 @@ describe PodLogsService do ...@@ -193,6 +197,26 @@ describe PodLogsService do
it_behaves_like 'success' it_behaves_like 'success'
end end
context 'when start and end time is specified' do
let(:pod_name) { 'some-pod' }
let(:container_name) { nil }
let(:start_time) { '2019-12-13T14:35:34.034Z' }
let(:end_time) { '2019-12-13T14:35:34.034Z' }
include_context 'return success'
it_behaves_like 'success'
end
context 'when start and end time are invalid' do
let(:pod_name) { 'some-pod' }
let(:container_name) { nil }
let(:start_time) { '1' }
let(:end_time) { '2' }
it_behaves_like 'error', 'Invalid start or end time format'
end
context 'when error is returned' do context 'when error is returned' do
include_context 'return error', 'Kubernetes API returned status code: 400' include_context 'return error', 'Kubernetes API returned status code: 400'
...@@ -204,7 +228,7 @@ describe PodLogsService do ...@@ -204,7 +228,7 @@ describe PodLogsService do
context 'when nil is returned' do context 'when nil is returned' do
before do before do
allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs) allow_any_instance_of(EE::Clusters::Platforms::Kubernetes).to receive(:read_pod_logs)
.with(environment.id, pod_name, environment.deployment_namespace, container: container_name, search: search) .with(environment.id, pod_name, environment.deployment_namespace, container: container_name, search: search, start_time: start_time, end_time: end_time)
.and_return(nil) .and_return(nil)
end end
......
...@@ -10227,6 +10227,9 @@ msgstr "" ...@@ -10227,6 +10227,9 @@ msgstr ""
msgid "Invalid server response" msgid "Invalid server response"
msgstr "" msgstr ""
msgid "Invalid start or end time format"
msgstr ""
msgid "Invalid two-factor code." msgid "Invalid two-factor code."
msgstr "" msgstr ""
......
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