Esempio n. 1
0
def test_history_monotonous(history_with_epoch_set):
    ts = history_with_epoch_set.epoch
    for delta in (Timedelta.from_s(1), Timedelta.from_string("20s")):
        new_ts = ts + delta
        history_with_epoch_set.insert(new_ts, State.OK)

        assert history_with_epoch_set.transitions[-1].time == new_ts
Esempio n. 2
0
    def _calculate_interval_durations(self, response_aggregates):
        # we assume LAST semantic, but this should not matter for equidistant intervals
        yield Timedelta(0)

        for previous_ta, current_ta in zip(
            response_aggregates, response_aggregates[1:]
        ):
            duration = current_ta.timestamp - previous_ta.timestamp
            # We need strong monotony. DB-HTA guarantees it currently
            assert duration > Timedelta(0)
            yield duration
Esempio n. 3
0
    def __init__(self, time_window: Optional[Timedelta]):
        """A history of state transitions for some metric, spanning at most a
        duration of ``time_window``.

        It contains a point in time called 'epoch' that signifies when the state
        that the first transition switched from was entered.  In a graph (t[0]
        is the first transition, t[1] the second etc.):

                  ┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮    ⋯
                  │  t[0].state  │     t[1].state     │
           ⋯──────┴──────────────┴────────────────────┴────────────→
                epoch        t[0].time            t[1].time       time
        """

        # The list of state transitions for this metric.
        self._transitions: List[StateTransition] = list()

        # The point in time at which we assume the metric entered the state
        # given by the first transition ``self._transitions[0]``, if present.
        # This is necessary as transitions have last semantics, and we otherwise
        # had to assume that it was in this state since forever (which skews
        # statistics significantly, as you can imagine).
        self._epoch: Optional[Timestamp] = None

        # Use a sensible default time window for keeping past transitions.
        self._time_window: Timedelta
        if time_window is None:
            self._time_window = Timedelta.from_s(30)
        else:
            if time_window.ns > 0:
                self._time_window = time_window
            else:
                raise ValueError(
                    "State transition history time window must be a positive duration"
                )
async def test_timeout_check_no_bump(timeout_check):
    await sleep(timeout_check._timeout + Timedelta.from_ms(10))

    callback = timeout_check._timeout_callback
    assert callback.called
    assert callback.last_timestamp is None
    assert callback.timeout == timeout_check._timeout
Esempio n. 5
0
def timedelta_random_list():
    """Generate a stream of random Timedeltas of different magnitudes, with
    varying amounts of trailing zeroes.  The durations (in nanoseconds) look
    like this::

               1 to 17 digits total
                  (num_digits)
                       |
            .----------+----------.
            xxxxxxxxx00000000000000
            '---+---'
                |
            1 to num_digits
          leading random digits
          (num_leading_digits)
    """
    rng = Random(72438990202596743)

    for num_digits in range(1, 17):
        for num_leading_digits in range(1, num_digits):
            random = rng.randrange(10**num_leading_digits,
                                   10**(num_leading_digits + 1))
            ts = random * (10**(num_digits - num_leading_digits))

            logger.debug(f"num_digits={num_digits:2}, "
                         f"num_leading_digits={num_leading_digits:2}, "
                         f"random={random:20}, "
                         f"ts={ts}, ")
            yield Timedelta(ts)
Esempio n. 6
0
def test_history_insert_with_epoch_set(history_with_epoch_set):
    # Inserting a transition into a history with an epoch already set should
    # make that transition the most recent transition of that history.
    ts = history_with_epoch_set.epoch + Timedelta.from_s(1)
    history_with_epoch_set.insert(ts, State.OK)

    assert history_with_epoch_set.transitions[-1].time == ts
Esempio n. 7
0
    def state_prevalences(self) -> Optional[Dict[State, float]]:
        """Return a ``dict`` where for each state, the share of time that a
        metric was in this state is a ``float`` between ``0.0`` and ``1.0``.
        """
        # We might only calculate prevalences of states if we already set an
        # epoch and there exists at least one transition.
        if self.is_empty():
            return None

        # Determine the first timestamp for calculating the cumulative duration
        # of each state.  Make sure it is at most self._time_window in the past,
        # wrt. the most recent transition in this history.
        latest_transition = self._transitions[-1]
        oldest_transition_time = max(
            latest_transition.time - self._time_window, self._epoch)

        total_duration: Timedelta = latest_transition.time - oldest_transition_time

        cumulative_durations = {state: Timedelta(0) for state in State}

        prev_time: Timestamp = oldest_transition_time
        current: StateTransition
        for current in self._transitions:
            cumulative_durations[current.state] += current.time - prev_time
            prev_time = current.time

        try:
            # Return the prevalence of each state as a percentage of the total
            # time spanned by all transitions.
            return {
                state: duration.ns / total_duration.ns
                for state, duration in cumulative_durations.items()
            }
        except ZeroDivisionError:
            return None
Esempio n. 8
0
def test_history_non_monotonous(history_with_epoch_set, delta, caplog):
    ts = history_with_epoch_set.epoch + Timedelta.from_s(1)
    history_with_epoch_set.insert(ts, State.OK)

    next_ts = ts + delta
    with caplog.at_level(logging.WARNING):
        history_with_epoch_set.insert(next_ts, State.OK)
        assert "Times of state transitions must be strictly increasing" in caplog.text
async def test_timeout_check_no_callback_after_cancel(timeout):
    class CallOnce(Callback):
        def __init__(self):
            self.called_before = False
            super().__init__()

        def __call__(self, *, timeout, last_timestamp):
            assert not self.called_before
            super().__call__(timeout=timeout, last_timestamp=last_timestamp)
            self.called_before = True

    timeout_check = TimeoutCheck(timeout=timeout, on_timeout=CallOnce())

    timeout_check.start()
    await sleep(timeout_check._timeout + Timedelta.from_ms(10))

    timeout_check.cancel()
    await sleep(timeout_check._timeout + Timedelta.from_ms(10))
Esempio n. 10
0
    def _metric():
        timestamp = Timestamp(0)
        delta = Timedelta.from_s(1)
        value = 0.0

        while True:
            yield (timestamp, value)
            timestamp += delta
            value += 1.0
Esempio n. 11
0
    def __init__(
        self,
        timeout: Timedelta,
        on_timeout: Coroutine,
        grace_period: Timedelta = Timedelta(0),
    ):
        self._timeout = timeout
        self._on_timeout_callback = on_timeout
        self._grace_period = grace_period

        self._last_timestamp: Optional[Timestamp] = None
        self._new_timestamp_event: Event = Event()
        self._task: asyncio.Task = asyncio.create_task(self._run())
        self._throttle = False
Esempio n. 12
0
class Ticker:
    DEFAULT_DELTA = Timedelta.from_s(1)
    DEFAULT_START = Timestamp(0)

    def __init__(self, delta=DEFAULT_DELTA, start=DEFAULT_START):
        self.delta = delta
        self.start = start
        self.now = self.start

    def __next__(self):
        now = self.now
        self.now += self.delta
        return now

    def __iter__(self):
        return self
Esempio n. 13
0
    def __init__(
        self,
        timeout: Timedelta,
        on_timeout: TimeoutCallback,
        grace_period: Optional[Timedelta] = None,
        name: Optional[str] = None,
    ):
        self._timeout = timeout
        self._timeout_callback: TimeoutCallback = on_timeout
        self._grace_period = Timedelta(
            0) if grace_period is None else grace_period
        self._name = name

        self._last_timestamp: Optional[Timestamp] = None
        self._new_timestamp_event: Event = Event()
        self._throttle = False
Esempio n. 14
0
    def __init__(
        self,
        name: str,
        metrics: Iterable[str],
        value_constraints: Optional[Dict[str, float]],
        timeout: Optional[str] = None,
        on_timeout: Optional[Coroutine] = None,
    ):
        """Create value- and timeout-checks for a set of metrics

        :param name: The name of this check
        :param metrics: Iterable of names of metrics to monitor
        :param value_constraints: Dictionary indicating warning and critical
            value ranges, see ValueCheck.  If omitted, this check does not care
            for which values its metrics report.
        :param timeout: If set, and a metric does not deliver values within
            this duration, run the callback on_timeout
        :param on_timeout: Callback to run when metrics do not deliver values
            in time, mandatory if timeout is given.
        """
        self._name = name
        self._metrics: Set[str] = set(metrics)

        self._status_cache = StatusCache(self._metrics)

        self._value_checks: Optional[Dict[str, ValueCheck]] = None
        self._timeout_checks: Optional[Dict[str, TimeoutCheck]] = None
        self._on_timeout_callback: Optional[Coroutine] = None

        if value_constraints is not None:
            self._value_checks: Dict[str, ValueCheck] = {
                metric: ValueCheck(**value_constraints) for metric in self._metrics
            }

        if timeout is not None:
            if on_timeout is None:
                raise ValueError("on_timeout callback is required if timeout is given")
            self._on_timeout_callback = on_timeout
            self._timeout_checks = {
                metric: TimeoutCheck(
                    Timedelta.from_string(timeout),
                    self._get_on_timeout_callback(metric),
                )
                for metric in self._metrics
            }
Esempio n. 15
0
def test_timeaggregate_from_value(timestamp):
    VALUE = 42.0
    agg = TimeAggregate.from_value(timestamp=timestamp, value=VALUE)

    assert agg.timestamp == timestamp
    assert agg.active_time == Timedelta(0)
    assert agg.count == 1

    assert agg.minimum == VALUE
    assert agg.maximum == VALUE
    assert agg.sum == VALUE
    assert agg.integral_ns == 0

    assert isclose(agg.mean, VALUE)
    assert isclose(agg.mean_sum, VALUE)

    with pytest.raises(ZeroDivisionError):
        agg.mean_integral
Esempio n. 16
0
 def convert(
     self,
     value: Union[str, Timedelta],
     param: Optional[Parameter],
     ctx: Optional[Context],
 ) -> Optional[Timedelta]:
     if value is None:
         return None
     elif isinstance(value, str):
         try:
             return Timedelta.from_string(value)
         except ValueError:
             self.fail(
                 'expected a duration: "<value>[<unit>]"',
                 param=param,
                 ctx=ctx,
             )
     else:
         return value
Esempio n. 17
0
def parse_functions(target_dict):
    for function in target_dict.get("functions", ["avg"]):
        if function == "avg":
            yield AvgFunction()
        elif function == "min":
            yield MinFunction()
        elif function == "max":
            yield MaxFunction()
        elif function == "count":
            yield CountFunction()
        elif function == "sma":
            try:
                yield MovingAverageFunction(
                    Timedelta.from_string(target_dict.get("sma_window"))
                )
            except (TypeError, KeyError):
                pass
        # Cannot instantiate RawFunction - it automatically replaces the aggregates when zooming in
        else:
            raise KeyError(f"Unknown function '{function}' requested")
Esempio n. 18
0
def soft_fail_cache():
    return StateCache(
        metrics=["publish.rate"],
        transition_debounce_window=Timedelta.from_string("1 day"),
        transition_postprocessor=SoftFail(max_fail_count=2),
    )
Esempio n. 19
0
 def __init__(self, minimum_duration: Union[Timedelta, str], **_kwargs):
     self._minimum_duration = (Timedelta.from_string(minimum_duration)
                               if isinstance(minimum_duration, str) else
                               minimum_duration)
Esempio n. 20
0
def time_delta_random():
    return Timedelta(8295638928)
Esempio n. 21
0
def test_timedelta_floordiv(ns: int, factor: int, expected_ns: int):
    timedelta = Timedelta(ns)

    assert (timedelta // factor) == Timedelta(expected_ns)
Esempio n. 22
0
    assert history_with_epoch_set.transitions[-1].time == ts


def test_history_monotonous(history_with_epoch_set):
    ts = history_with_epoch_set.epoch
    for delta in (Timedelta.from_s(1), Timedelta.from_string("20s")):
        new_ts = ts + delta
        history_with_epoch_set.insert(new_ts, State.OK)

        assert history_with_epoch_set.transitions[-1].time == new_ts


@pytest.mark.parametrize(
    "delta",
    [
        Timedelta(0),
        Timedelta.from_s(-1),
    ],
)
def test_history_non_monotonous(history_with_epoch_set, delta, caplog):
    ts = history_with_epoch_set.epoch + Timedelta.from_s(1)
    history_with_epoch_set.insert(ts, State.OK)

    next_ts = ts + delta
    with caplog.at_level(logging.WARNING):
        history_with_epoch_set.insert(next_ts, State.OK)
        assert "Times of state transitions must be strictly increasing" in caplog.text


@pytest.mark.parametrize(
    "ticker",
def timeout() -> Timedelta:
    return Timedelta.from_ms(100)
Esempio n. 24
0
def test_timedelta_truediv(ns: int, factor: Union[int, float],
                           expected_ns: int):
    timedelta = Timedelta(ns)

    assert (timedelta / factor) == Timedelta(expected_ns)
Esempio n. 25
0
    rpc: AsyncMock


@pytest.fixture
def interval_source():

    with patch("metricq.interval_source.IntervalSource.rpc"):
        source = _TestIntervalSource(token="source-interval-test",
                                     management_url="amqps://test.invalid")
        yield source


@pytest.mark.parametrize(
    ("period", "normalized"),
    [
        (Timedelta(1337), Timedelta(1337)),
        (4, Timedelta.from_s(4)),
    ],
)
def test_period_setter_normalizing(interval_source: _TestIntervalSource,
                                   period, normalized):
    """Internally, the interval source period is normalized to a Timedelta"""
    assert interval_source.period is None

    interval_source.period = period
    assert interval_source.period == normalized


def test_period_no_reset(interval_source: _TestIntervalSource):
    """Currently, the interval source period cannot be reset to None"""
def tick() -> Timedelta:
    return Timedelta.from_s(0.1)
Esempio n. 27
0
 def __init__(self):
     self.interval = Timedelta(0)
Esempio n. 28
0
    def transform_data(self, response):
        if len(response) == 0:
            return []

        response_aggregates = list(response.aggregates(convert=True))

        ma_integral_ns = 0
        ma_active_time = Timedelta(0)
        ma_begin_index = 1
        ma_begin_time = response_aggregates[0].timestamp
        ma_end_index = 1
        ma_end_time = response_aggregates[0].timestamp

        interval_durations = list(
            self._calculate_interval_durations(response_aggregates)
        )

        for timeaggregate, current_interval_duration in zip(
            response_aggregates, interval_durations
        ):
            # The moving average window is symmetric around the current *interval* - not the current point

            # How much time is covered by the current interval width and how much is on both sides "outside"
            assert current_interval_duration >= Timedelta(0)
            outside_duration = self.interval - current_interval_duration
            # If the current interval is wider than the target moving average window, just use the current one
            outside_duration = max(Timedelta(0), outside_duration)
            seek_begin_time = (
                timeaggregate.timestamp
                - current_interval_duration
                - outside_duration / 2
            )
            seek_end_time = timeaggregate.timestamp + outside_duration / 2

            # TODO unify these two loops
            # Move left part of the window
            while ma_begin_time < seek_begin_time:
                next_step_time = min(
                    response_aggregates[ma_begin_index].timestamp, seek_begin_time
                )
                step_duration = next_step_time - ma_begin_time
                # scale can be 0 (everything is nop),
                # 1 (full interval needs to be removed), or something in between
                scale = step_duration.ns / interval_durations[ma_begin_index].ns
                ma_active_time -= (
                    response_aggregates[ma_begin_index].active_time * scale
                )
                ma_integral_ns -= (
                    response_aggregates[ma_begin_index].integral_ns * scale
                )

                ma_begin_time = next_step_time
                assert ma_begin_time <= response_aggregates[ma_begin_index].timestamp
                if ma_begin_time == response_aggregates[ma_begin_index].timestamp:
                    # Need to move to the next interval
                    ma_begin_index += 1

            # Move right part of the window
            while ma_end_time < seek_end_time and ma_end_index < len(
                response_aggregates
            ):
                next_step_time = min(
                    response_aggregates[ma_end_index].timestamp, seek_end_time
                )
                step_duration = next_step_time - ma_end_time
                # scale can be 0 (everything is nop),
                # 1 (full interval needs to be removed), or something in between
                scale = step_duration.ns / interval_durations[ma_end_index].ns
                ma_active_time += response_aggregates[ma_end_index].active_time * scale
                ma_integral_ns += response_aggregates[ma_end_index].integral_ns * scale

                ma_end_time = next_step_time
                assert ma_end_time <= response_aggregates[ma_end_index].timestamp
                if ma_end_time == response_aggregates[ma_end_index].timestamp:
                    # Need to move to the next interval
                    ma_end_index += 1

            if seek_begin_time != ma_begin_time or seek_end_time != ma_end_time:
                # Interval window not complete
                continue
            if ma_active_time == 0:
                continue

            yield timeaggregate.timestamp, ma_integral_ns / ma_active_time.ns
Esempio n. 29
0
def test_duration_param():
    value = "30s"
    DURATION = DurationParam(default=None)
    assert DURATION.convert(value, param=None,
                            ctx=None) == Timedelta.from_string(value)
Esempio n. 30
0
def test_timedelta_from_string(input, expected_ns):
    assert Timedelta.from_string(input) == Timedelta(expected_ns)