Skip to content

Ambiguous Timestamp Handling - #92

Merged
yaauie merged 10 commits into
logstash-plugins:mainfrom
yaauie:ambiguous-timestamp-handling
Oct 11, 2022
Merged

yaauie merged 10 commits into
logstash-plugins:mainfrom
yaauie:ambiguous-timestamp-handling

Conversation

@yaauie

@yaauie yaauie commented Dec 1, 2021

Copy link
Copy Markdown
Contributor

Adds support for timezones provided to jdbc_default_timezone to include instructions for how to handle ambiguous times from daylight-savings related overlaps.

The SQL Timestamp column type does not contain offset information, so when it is used to store local times and those local times come from a timezone with seasonal daylight savings transitions, there is the possibility for a timestamp to be ambiguous.

When reading a timestamp from such a column that cannot be unambiguously resolved to a single point on an ordered and continuous timeline, a TZInfo::AmbiguousTime has been raised mid-processing, causing a Sequel::InvalidValue to be raised and subsequent rows in the result set to not be emitted.

This changeset allows a user to configure DST-related disambiguation when specifying their jdbc_default_timezone. When encountering an ambiguous timestamp in the 2 overlapping hours surrounding the transition from Daylight Savings time to Standard Time:

  • jdbc_default_timezone => "America/Los_Angeles[dst_enabled_on_overlap:true]": ambiguous timestamps is assumed to be from before the transition point
  • jdbc_default_timezone => "America/Los_Angeles[dst_enabled_on_overlap:false]": ambiguous timestamps is assumed to be from after the transition point
  • jdbc_default_timezone => "America/Los_Angeles": ambiguous timestamps results in query failure

@karenzone karenzone left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs review: Nice explanations here! I offered some minor wording and formatting suggestions to make your content pop.

Without formatting changes:
Screen Shot 2021-12-01 at 3 47 34 PM

With formatting changes:
Screen Shot 2021-12-01 at 4 42 59 PM

Comment thread docs/input-jdbc.asciidoc Outdated
Comment thread docs/input-jdbc.asciidoc Outdated
Comment thread docs/input-jdbc.asciidoc
Comment thread CHANGELOG.md Outdated
Co-authored-by: Karen Metts <35154725+karenzone@users.noreply.github.com>

@karenzone karenzone left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left suggestions inline. This content will perform better in SEO rankings if these are formatted as subheadings.
Otherwise, docs LGTM.

Comment thread docs/input-jdbc.asciidoc Outdated
Comment thread docs/input-jdbc.asciidoc Outdated
Comment thread docs/input-jdbc.asciidoc Outdated

@kares kares left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥇 so great to finally see ambiguous DST timestamps being handled ...

no concerns implementation wise assuming we are in agreement with the way to name and configure the feature.

left a comment to consider our options bellow.

Comment thread spec/inputs/jdbc_spec.rb
end

context "when initialized with a preference for DST being enabled" do
let(:jdbc_default_timezone) { 'America/Chicago[dst_enabled_on_overlap:true]' }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what I like here that we're keeping the dst handling information close to the timezone, what I do not like is that we extend the TZ string I know it's unlikely but what happens if users request a different behavior or smt else needs to be extended in the time-zone.

another thing to consider, since time is ambiguous twice a year I wonder if users would ever want to have different behavior (once use dst, second time fallback to non dst time) - a bit far fetched, why anyone would want that, but we should keep our options open.

ways forward seems to be:

  1. have a separate configuration option e.g. timezone_dst_enabled_on_overlap => true/false
    or smt of a ambiguous_timezone_local_time_handling => use_dst|use_non_dst|error
  2. since the flag relates to time zone maybe extend the option to accepts a string_or_hash e.g.
input {
  jdbc {
    jdbc_connection_string => "..."
    jdbc_default_timezone => {
      name => 'America/Chicago',
      
      ambiguous_local_time_handling => dst
      # or
      dst_enabled_on_overlap => true
      
    }
    use_column_value => true
    # ...
  }
}
  1. extend the jdbc_default_timezone with custom parsing [] (current PR status e.g. America/Chicago[dst_enabled_on_overlap:true])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I attempted and ruled out the first option (separate config entirely) because it is just too separate and users aren't likely to find it until after they have run into a DST-transition issue.

I have not yet made a serious attempt at the second (string_or_hash). On the surface I like it, but I believe it would benefit from a validator hack to ensure we keep error messages from malformed instructions close to the user. I will make an attempt at this, and determine if it is cleared in practice.

This PR implements the third option and attempts to remain open to future extension:

  • Named timezones are effectively constrained to slashes (/), ASCII letters (a-z and A-Z), dashes (-), dots (.), and underscores (_); by defining extensions enclosed in square brackets, we are immune to upstream changes to the timezonedb
  • Our implementation only uses two hard-coded extensions in order to avoid writing a complex parser, but would easily accept multiple semicolon-separated extensions.
  • After many attempts at wording, I decided to stick with ruby stdlib's naming dst_enabled_on_overlap, because this extension only handles DST-related ambiguity (and not ambiguity caused by political-upheaval).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whether we do the second or third approach, adding a validator brings errors closer to the user and is easy enough:

    module JDBCTimezoneSpecValidator
      def validate_value(value, validator_name)
        return super(value, validator_name) unless validator_name == :jdbc_timezone_spec

        return true, value if value.kind_of?(::TZInfo::Timezone) # multiple coercion passes
        return true, nil if value.nil? || (value.kind_of?(String) && value.empty?)

        [true, TimezoneProxy.load(value)] rescue [false, $!.message]
      end
    end

AS to the implementation of accepting either jdbc_default_timezone => "America/Los_Angeles" or jdbc_default_timezone => { name => "America/Los_Angeles" dst_enabled_on_overlap => true }, that is also straight-forward enough:

    def self.load(spec)
      return spec if spec.kind_of?(::TZInfo::Timezone)

      if spec.kind_of?(Hash)
        extensions = spec.dup
        name = extensions.delete("name") { fail(ArgumentError, "timezone must include 'name' key") }
      else
        name = spec
        extensions = {}
      end

      timezone = ::TZInfo::Timezone.get(name)

      if extensions && extensions.include?("dst_enabled_on_overlap")
        dst_enabled_on_overlap = extensions.delete("dst_enabled_on_overlap")
        case dst_enabled_on_overlap.to_s.downcase
        when 'true'  then timezone = timezone.dup.extend(PeriodForLocalWithDSTPreference::ON)
        when 'false' then timezone = timezone.dup.extend(PeriodForLocalWithDSTPreference::OFF)
        else fail(ArgumentError, "Invalid timezone extension `dst_enabled_on_overlap:#{dst_enabled_on_overlap}`")
        end
      end

      fail(ArgumentError, "Unexpected timezone extension: #{extensions}") unless extensions.empty?

      timezone
    end

Interface-wise, I think the square-bracket approach is a bit more straight-forward because the pipeline syntax for maps isn't always clear (whitespace separation, not commas):

input {
  jdbc {
    jdbc_connection_string => "..."
    jdbc_default_timezone => {
      name => "America/Los_Angeles"
      dst_enabled_on_overlap => true
    }
    use_column_value => true
    # ...
  }
}

VS

input {
  jdbc {
    jdbc_connection_string => "..."
    jdbc_default_timezone => "America/Los_Angeles[dst_enabled_on_overlap:true]"
    use_column_value => true
    # ...
  }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for looking into this.
fine by shipping the current approach given that the risk of additional time-zone extensions is low (which would blow off the string e.g. America/Los_Angeles[dst_enabled_on_overlap:true][another_extension_on_dst_switch:+1h]) I have no further concerns.

In the Input, `jdbc_default_timezone` is coerced to a `::TZInfo::Timezone`
instance during plugin instantiation using a custom validator extension wired
up to `TimezoneProxy::load`. This ensures that any issues having to do with
the timezone's specification are revealed to the user during plugin
instantiation instead of at runtime.

Additionally, Sequel::InvalidValue exceptions no longer crash the input.
@yaauie
yaauie requested a review from kares December 6, 2021 21:53

@kares kares left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💅 how the implementation wraps the TZInfo::Timezone object,
this allows multiple jdbc plugins to function in isolation as expected (opposed to setting a global Sequel.tzinfo_disambiguator)

Comment thread spec/inputs/jdbc_spec.rb Outdated
Comment thread spec/inputs/jdbc_spec.rb Outdated
# puts("LOGGER: METHOD(#{plugin.logger.inspect}) IVAR(#{logger.inspect})")
plugin.run(queue)
# expect(plugin.logger).to have_received(:warn) { |*actual_args| puts "WARN>>#{actual_args.inspect}" }
expect(plugin.logger).to have_received(:warn).with(a_string_including("Exception when executing JDBC query"), a_hash_including(:exception => a_string_including("2021-11-07 01:23:45 is an ambiguous local time")))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍 the assert, would the commented bits be still useful later on?

Comment thread spec/inputs/jdbc_spec.rb Outdated
Comment thread spec/inputs/jdbc_spec.rb Outdated
Co-authored-by: Karen Metts <35154725+karenzone@users.noreply.github.com>
Co-authored-by: Karol Bucek <kares@users.noreply.github.com>
@yaauie
yaauie force-pushed the ambiguous-timestamp-handling branch from bcbdcfd to 3591760 Compare April 20, 2022 19:12
@yaauie
yaauie force-pushed the ambiguous-timestamp-handling branch 2 times, most recently from c96e385 to 11854c3 Compare October 4, 2022 00:24
@yaauie
yaauie force-pushed the ambiguous-timestamp-handling branch from 11854c3 to f870e19 Compare October 4, 2022 00:27
@yaauie

yaauie commented Oct 4, 2022

Copy link
Copy Markdown
Contributor Author

Changes since last approval:

  • merging upstream/main 17c4123, conflicts resolved by:
  • minor refinement of specs to match actual user input
  • bump gradle to support Java 17 (and therefore Logstash 8.4+)
  • fix warnings about using BigDecimal in specs

@roaksoax roaksoax left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@yaauie
yaauie merged commit 7c9f11d into logstash-plugins:main Oct 11, 2022
@yaauie
yaauie deleted the ambiguous-timestamp-handling branch October 11, 2022 15:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants