Commit f89237f0 authored by Stan Hu's avatar Stan Hu

Make Sidekiq timestamps consistently ISO 8601

Previously when an exception occurred in Sidekiq, Sidekiq would export
logs with timestamps (e.g. created_at, enqueued_at) in floating point
seconds, while other jobs would report in ISO 8601 format. This
inconsistency in data types would cause Elasticsearch to drop logs that
did not match the schema type (date in most cases).

This commit moves the responsibility of formatting timestamps to the
Sidekiq JSON formatter where it properly belongs. The job logger now
generates timestamps with floats, just as Sidekiq does. This ensures
that timestamps are manipulated only in one place.

See https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/8269
parent 6e88bebc
---
title: Make Sidekiq timestamps consistently ISO 8601
merge_request: 22750
author:
type: fixed
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Gitlab module Gitlab
module SidekiqLogging module SidekiqLogging
class JSONFormatter class JSONFormatter
TIMESTAMP_FIELDS = %w[created_at enqueued_at started_at retried_at failed_at completed_at].freeze
def call(severity, timestamp, progname, data) def call(severity, timestamp, progname, data)
output = { output = {
severity: severity, severity: severity,
...@@ -13,11 +15,27 @@ module Gitlab ...@@ -13,11 +15,27 @@ module Gitlab
when String when String
output[:message] = data output[:message] = data
when Hash when Hash
convert_to_iso8601!(data)
output.merge!(data) output.merge!(data)
end end
output.to_json + "\n" output.to_json + "\n"
end end
private
def convert_to_iso8601!(payload)
TIMESTAMP_FIELDS.each do |key|
value = payload[key]
payload[key] = format_time(value) if value.present?
end
end
def format_time(timestamp)
return timestamp unless timestamp.is_a?(Numeric)
Time.at(timestamp).utc.iso8601(3)
end
end end
end end
end end
...@@ -6,8 +6,6 @@ require 'active_record/log_subscriber' ...@@ -6,8 +6,6 @@ require 'active_record/log_subscriber'
module Gitlab module Gitlab
module SidekiqLogging module SidekiqLogging
class StructuredLogger class StructuredLogger
START_TIMESTAMP_FIELDS = %w[created_at enqueued_at].freeze
DONE_TIMESTAMP_FIELDS = %w[started_at retried_at failed_at completed_at].freeze
MAXIMUM_JOB_ARGUMENTS_LENGTH = 10.kilobytes MAXIMUM_JOB_ARGUMENTS_LENGTH = 10.kilobytes
def call(job, queue) def call(job, queue)
...@@ -65,8 +63,6 @@ module Gitlab ...@@ -65,8 +63,6 @@ module Gitlab
payload['job_status'] = 'done' payload['job_status'] = 'done'
end end
convert_to_iso8601(payload, DONE_TIMESTAMP_FIELDS)
payload['db_duration'] = ActiveRecord::LogSubscriber.runtime payload['db_duration'] = ActiveRecord::LogSubscriber.runtime
payload['db_duration_s'] = payload['db_duration'] / 1000 payload['db_duration_s'] = payload['db_duration'] / 1000
...@@ -79,7 +75,7 @@ module Gitlab ...@@ -79,7 +75,7 @@ module Gitlab
# ignore `cpu_s` if the platform does not support Process::CLOCK_THREAD_CPUTIME_ID (time[:cputime] == 0) # ignore `cpu_s` if the platform does not support Process::CLOCK_THREAD_CPUTIME_ID (time[:cputime] == 0)
# supported OS version can be found at: https://www.rubydoc.info/stdlib/core/2.1.6/Process:clock_gettime # supported OS version can be found at: https://www.rubydoc.info/stdlib/core/2.1.6/Process:clock_gettime
payload['cpu_s'] = time[:cputime].round(6) if time[:cputime] > 0 payload['cpu_s'] = time[:cputime].round(6) if time[:cputime] > 0
payload['completed_at'] = Time.now.utc payload['completed_at'] = Time.now.utc.to_f
end end
def parse_job(job) def parse_job(job)
...@@ -91,17 +87,9 @@ module Gitlab ...@@ -91,17 +87,9 @@ module Gitlab
job.delete('args') unless ENV['SIDEKIQ_LOG_ARGUMENTS'] job.delete('args') unless ENV['SIDEKIQ_LOG_ARGUMENTS']
job['args'] = limited_job_args(job['args']) if job['args'] job['args'] = limited_job_args(job['args']) if job['args']
convert_to_iso8601(job, START_TIMESTAMP_FIELDS)
job job
end end
def convert_to_iso8601(payload, keys)
keys.each do |key|
payload[key] = format_time(payload[key]) if payload[key]
end
end
def elapsed(t0) def elapsed(t0)
t1 = get_time t1 = get_time
{ {
...@@ -121,12 +109,6 @@ module Gitlab ...@@ -121,12 +109,6 @@ module Gitlab
Gitlab::Metrics::System.monotonic_time Gitlab::Metrics::System.monotonic_time
end end
def format_time(timestamp)
return timestamp if timestamp.is_a?(String)
Time.at(timestamp).utc.iso8601(3)
end
def limited_job_args(args) def limited_job_args(args)
return unless args.is_a?(Array) return unless args.is_a?(Array)
......
...@@ -3,28 +3,53 @@ ...@@ -3,28 +3,53 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::SidekiqLogging::JSONFormatter do describe Gitlab::SidekiqLogging::JSONFormatter do
let(:hash_input) { { foo: 1, bar: 'test' } }
let(:message) { 'This is a test' } let(:message) { 'This is a test' }
let(:timestamp) { Time.now } let(:now) { Time.now }
let(:timestamp) { now.utc.to_f }
it 'wraps a Hash' do let(:timestamp_iso8601) { now.iso8601(3) }
result = subject.call('INFO', timestamp, 'my program', hash_input)
describe 'with a Hash' do
data = JSON.parse(result) let(:hash_input) do
expected_output = hash_input.stringify_keys {
expected_output['severity'] = 'INFO' foo: 1,
expected_output['time'] = timestamp.utc.iso8601(3) 'bar' => 'test',
'created_at' => timestamp,
expect(data).to eq(expected_output) 'enqueued_at' => timestamp,
'started_at' => timestamp,
'retried_at' => timestamp,
'failed_at' => timestamp,
'completed_at' => timestamp_iso8601
}
end
it 'properly formats timestamps into ISO 8601 form' do
result = subject.call('INFO', now, 'my program', hash_input)
data = JSON.parse(result)
expected_output = hash_input.stringify_keys.merge!(
{
'severity' => 'INFO',
'time' => timestamp_iso8601,
'created_at' => timestamp_iso8601,
'enqueued_at' => timestamp_iso8601,
'started_at' => timestamp_iso8601,
'retried_at' => timestamp_iso8601,
'failed_at' => timestamp_iso8601,
'completed_at' => timestamp_iso8601
}
)
expect(data).to eq(expected_output)
end
end end
it 'wraps a String' do it 'wraps a String' do
result = subject.call('DEBUG', timestamp, 'my string', message) result = subject.call('DEBUG', now, 'my string', message)
data = JSON.parse(result) data = JSON.parse(result)
expected_output = { expected_output = {
severity: 'DEBUG', severity: 'DEBUG',
time: timestamp.utc.iso8601(3), time: timestamp_iso8601,
message: message message: message
} }
......
...@@ -30,8 +30,8 @@ describe Gitlab::SidekiqLogging::StructuredLogger do ...@@ -30,8 +30,8 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
'message' => 'TestWorker JID-da883554ee4fe414012f5f42: start', 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: start',
'job_status' => 'start', 'job_status' => 'start',
'pid' => Process.pid, 'pid' => Process.pid,
'created_at' => created_at.iso8601(3), 'created_at' => created_at.to_f,
'enqueued_at' => created_at.iso8601(3), 'enqueued_at' => created_at.to_f,
'scheduling_latency_s' => scheduling_latency_s 'scheduling_latency_s' => scheduling_latency_s
) )
end end
...@@ -40,7 +40,7 @@ describe Gitlab::SidekiqLogging::StructuredLogger do ...@@ -40,7 +40,7 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
'message' => 'TestWorker JID-da883554ee4fe414012f5f42: done: 0.0 sec', 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: done: 0.0 sec',
'job_status' => 'done', 'job_status' => 'done',
'duration' => 0.0, 'duration' => 0.0,
'completed_at' => timestamp.iso8601(3), 'completed_at' => timestamp.to_f,
'cpu_s' => 1.111112, 'cpu_s' => 1.111112,
'db_duration' => 0, 'db_duration' => 0,
'db_duration_s' => 0 'db_duration_s' => 0
...@@ -227,17 +227,17 @@ describe Gitlab::SidekiqLogging::StructuredLogger do ...@@ -227,17 +227,17 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
describe '#add_time_keys!' do describe '#add_time_keys!' do
let(:time) { { duration: 0.1231234, cputime: 1.2342345 } } let(:time) { { duration: 0.1231234, cputime: 1.2342345 } }
let(:payload) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status' } } let(:payload) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status' } }
let(:current_utc_time) { '2019-09-23 10:00:58 UTC' } let(:current_utc_time) { Time.now.utc }
let(:payload_with_time_keys) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status', 'duration' => 0.123123, 'cpu_s' => 1.234235, 'completed_at' => current_utc_time } } let(:payload_with_time_keys) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status', 'duration' => 0.123123, 'cpu_s' => 1.234235, 'completed_at' => current_utc_time.to_f } }
subject { described_class.new } subject { described_class.new }
it 'update payload correctly' do it 'update payload correctly' do
expect(Time).to receive_message_chain(:now, :utc).and_return(current_utc_time) Timecop.freeze(current_utc_time) do
subject.send(:add_time_keys!, time, payload)
subject.send(:add_time_keys!, time, payload) expect(payload).to eq(payload_with_time_keys)
end
expect(payload).to eq(payload_with_time_keys)
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