فرض کنید چهار بار مقدارِ شمارندهٔ تکمیلِ کار را می‌خوانیم: ۸، ۱۱، ۳ و ۵. تفاضلِ آخر و اول، منفیِ سه است؛ اما شمارِ کارهای انجام‌شده نمی‌تواند منفی باشد. افتِ مقدار شاید از بازنشانی آمده باشد. در این درس، با یک مدلِ محدود و دادهٔ ساختگی نشان می‌دهیم چگونه کمترین تعدادِ رخدادِ سازگار با مشاهده‌ها را حساب کنیم و چرا این عدد، تاریخچهٔ واقعی را بازسازی نمی‌کند. هیچ سرویس، سامانهٔ پایش یا ترافیکِ واقعی اجرا نمی‌شود.

۱. شمارنده را با تعدادِ جاری اشتباه نگیریم

شمارندهٔ تجمعی را از مقدارِ جاری جدا کنیم
تصویر مفهومی: شمارندهٔ تجمعی را از مقدارِ جاری جدا کنیم؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

مستندِ نوعِ Counter در Prometheus شمارندهٔ تجمعی را از Gauge جدا می‌کند. تعدادِ کلِ کارهای تکمیل‌شده می‌تواند شمارنده باشد؛ تعدادِ کارهای اکنون در صف، ممکن است بالا و پایین برود و همان قرارداد را ندارد. دیدنِ یک عددِ صحیح، برای انتخابِ نوعِ معیار کافی نیست. باید معلوم باشد چه رخدادی، در کدام منبع و با چه مرزِ شمارشی آن را افزایش می‌دهد.

مدلِ خودمان فقط دو عمل دارد: رخدادِ واحد، مقدار را یک واحد زیاد می‌کند؛ بازنشانی، مقدار را بدونِ افزودنِ رخداد به صفر می‌برد. مقدار منفی، کم‌کردنِ عادی، سرریز و چرخشِ شمارنده در این مدل نیست. بخشِ Counter در OpenMetrics 1.0 نیز افزایشِ تجمعی و امکانِ بازنشانی به صفر را مشخص می‌کند. این درس فقط مشاهده‌های عددی را می‌گیرد؛ زمانِ ایجاد، برچسب‌ها و قالبِ پیامِ آن استاندارد را پیاده نمی‌کند.

ورودی، یک tuple دقیق با دو تا شانزده مشاهدهٔ مرتب از یک هویتِ ثابت است. هر مقدار، int دقیق در بازهٔ صفر تا ۶۴ است؛ bool، زیرکلاسِ عدد، اعشار و رشته رد می‌شوند. ترتیبِ ورودی، ترتیبِ مشاهده فرض شده است؛ کد زمان‌مهر ندارد و ترتیب را از شبکه کشف نمی‌کند. واحدِ خروجی «تعدادِ رخداد» است؛ نه رخدادبرثانیه، نه احتمالِ خطا و نه زمانِ پاسخ.

۲. افتِ مشاهده‌شده را چگونه حساب کنیم؟

افتِ مقدار، نیازمند قراردادِ بازنشانی است
تصویر مفهومی: افتِ مقدار، نیازمند قراردادِ بازنشانی است؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

برای هر دو مشاهدهٔ پیاپی، اگر مقدارِ بعدی کمتر نباشد، کمترین افزایش برابرِ تفاضل است. اگر کمتر باشد، تحتِ فرض‌های مدل دست‌کم یک بازنشانی لازم است و کمترین تعدادِ رخداد برابرِ مقدارِ بعدی خواهد بود. برای ۸ به ۱۱، سه رخداد کافی است؛ برای ۱۱ به ۳، یک بازنشانیِ بی‌هزینه و سپس سه رخداد؛ و برای ۳ به ۵، دو رخداد. جمعِ سه، سه و دو، هشت می‌شود.

این یک کرانِ پایینِ قابلِ تحقق است. در حالتِ بدونِ افت، هر مسیرِ بدونِ بازنشانی همان تفاضل را لازم دارد؛ اگر بازنشانی رخ داده باشد، ساختنِ مقدارِ بعدی از صفر ارزان‌تر از آن تفاضل نمی‌شود. در حالتِ افت، پس از آخرین بازنشانی دست‌کم به تعدادِ مقدارِ نهایی رخداد لازم داریم. افزودنِ رخداد پیش از بازنشانی، فقط شمارِ واقعی را بیشتر می‌کند؛ برای رسیدن به حداقل، آن رخدادهای اضافی را انتخاب نمی‌کنیم.

مقدارِ نخست، موجودیِ شمارنده هنگامِ شروعِ مشاهده است؛ رخدادهای پیش از آن را به بازه اضافه نمی‌کنیم. برای ۴۰ به ۴۲، حداقل دو رخداد داریم، نه چهل‌ودو. همچنین ۸ به صفر یک افتِ دیده‌شده دارد، ولی حداقلِ رخدادِ بینِ دو مشاهده صفر است: بازنشانی به‌تنهایی این تغییر را می‌سازد. شمارِ افت‌ها را جدا گزارش می‌کنیم تا با شمارِ رخدادها یکی نشود.

۳. بازنشانیِ پنهان و خطای جمع‌زدنِ زودهنگام

ترکیبِ زودهنگام می‌تواند مرزِ دو منبع را پنهان کند
تصویر مفهومی: ترکیبِ زودهنگام می‌تواند مرزِ دو منبع را پنهان کند؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

دو مشاهدهٔ ۵ و ۷، چند تاریخچهٔ متفاوت را می‌پذیرند. می‌توان فقط دو رخداد داشت؛ یا بازنشانی کرد و هفت رخداد افزود؛ یا این بازنشانی و هفت رخداد را دوبار تکرار کرد. هر سه به مشاهدهٔ نهاییِ هفت می‌رسند، اما شمارِ رخدادها دو، هفت و چهارده و شمارِ بازنشانی‌ها صفر، یک و دو است. پس صفر بودنِ افتِ دیده‌شده، نبودِ بازنشانی را اثبات نمی‌کند و این دو مشاهده، کرانِ بالای متناهی برای همهٔ تاریخچه‌های مجاز نمی‌دهند.

حتی وقتی افت دیده می‌شود، رخدادهای قبل از آن ممکن است گم شوند. برای ۸ به ۳، بازنشانی و سه افزایش کافی است؛ اما دو افزایش تا ده، سپس بازنشانی و سه افزایش نیز همان دو مشاهده را می‌دهد و پنج رخداد دارد. بنابراین نامِ خروجی را «کمترین رخدادِ سازگار» گذاشته‌ایم. قراردادِ resets در Prometheus افتِ نمونه‌های عددیِ پیاپی را بازنشانی تفسیر می‌کند؛ این تفسیر، مشاهدهٔ همهٔ اتفاق‌های میانِ نمونه‌برداری‌ها نیست.

هویتِ منبع را هم زود حذف نکنید. منبعِ الف از ۸ به ۳ و منبعِ ب از ۲ به ۷ می‌رود؛ حداقل‌های جداگانه، سه و پنج‌اند و جمعشان هشت است. اگر ابتدا دو مقدارِ هم‌زمان را جمع کنیم، فقط ۱۰ به ۱۰ می‌بینیم و افت پنهان می‌شود. راهنمای rate نیز ترتیبِ محاسبهٔ نرخ و سپس تجمیع را توضیح می‌دهد. مثالِ ما صرفاً نمونهٔ نقضِ حسابی است و اجرای آن تابع نیست.

۴. کد کامل و مرجعِ مستقلِ مسیرهای کم‌هزینه

انتظارِ مستقل را با مسیرهای مجاز بسنجیم
تصویر مفهومی: انتظارِ مستقل را با مسیرهای مجاز بسنجیم؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

فایل را با نامِ counter_reset_observation.py ذخیره کنید. اعتبارسنجیِ همهٔ مقدارها پیش از محاسبه انجام می‌شود. تابع، ورودی را تغییر نمی‌دهد و افزایشِ هر فاصله، جایگاهِ صفرمبنای افت‌ها و جمع را برمی‌گرداند. این خروجی برای بازبینیِ قراردادِ مشاهده است؛ از آن نباید نرخِ یک سرویس یا هشدارِ عملیاتی ساخت، چون زمانِ واقعی و سیاستِ نمونه‌برداری در ورودی وجود ندارد.

"""Own finite counter-observation model, not Prometheus rate/increase parity."""
import heapq
import itertools
import json
import platform


MAX_SAMPLE = 64
MAX_OBSERVATIONS = 16


def checked_samples(samples):
    if type(samples) is not tuple:
        raise TypeError('An exact tuple of ordered counter observations is required')
    if not 2 <= len(samples) <= MAX_OBSERVATIONS:
        raise ValueError('The own finite profile requires2..16 observations')
    for value in samples:
        if type(value) is not int:
            raise TypeError('Exact int observations only; bool/subclasses are excluded')
        if not 0 <= value <= MAX_SAMPLE:
            raise ValueError('Observation outside own0..64 profile')
    return samples


def observed_increase(samples):
    """Minimum event count under unit increments and free reset-to-zero only.

    The first value is an initial observation, not events inside the interval.
    Observed drops are evidence under the model, not the actual reset count.
    """
    values = checked_samples(samples)
    increments = []
    drop_positions = []
    for position in range(1, len(values)):
        previous, current = values[position - 1], values[position]
        if current < previous:
            increments.append(current)
            drop_positions.append(position)
        else:
            increments.append(current - previous)
    return {
        'minimum_compatible_events': sum(increments),
        'per_observed_interval': tuple(increments),
        'observed_drop_positions_zero_based': tuple(drop_positions),
        'observed_drop_count': len(drop_positions),
    }


def graph_costs(start, cap):
    """Independent shortest paths: states0..cap, +1cost1, reset->0cost0.

    No observed_increase call and no pairwise difference/reset formula.
    Keeping the state cap at the largest observed value is sufficient for
    a minimum: an excursion above both endpoints before a reset only costs
    extra unit increments. The oracle is for small finite fixtures only.
    """
    distances = [None] * (cap + 1)
    distances[start] = 0
    pending = [(0, start)]
    while pending:
        cost, state = heapq.heappop(pending)
        if distances[state] != cost:
            continue
        edges = [(0, 0)]
        if state < cap:
            edges.append((state + 1, 1))
        for following, edge_cost in edges:
            candidate = cost + edge_cost
            if distances[following] is None or candidate < distances[following]:
                distances[following] = candidate
                heapq.heappush(pending, (candidate, following))
    return tuple(distances)


def replay_history(initial, actions):
    """Small explicit witness histories; not observed by the main model."""
    state = initial
    events = 0
    resets = 0
    for action, amount in actions:
        if action == 'increment':
            assert type(amount) is int and amount >= 0
            state += amount
            events += amount
        elif action == 'reset':
            assert amount == 0
            state = 0
            resets += 1
        else:
            raise AssertionError('Unknown internal witness action')
    return {'last_observation': state, 'actual_witness_events': events,
            'actual_witness_resets': resets}


def raises(call, exception):
    try:
        call()
    except exception:
        return True
    return False


def run_example():
    samples = (8, 11, 3, 5)
    original = samples
    result = observed_increase(samples)
    checks = {}
    checks['hand_fixture_eight_events_not_naive_minus_three'] = (
        result == {'minimum_compatible_events': 8,
                   'per_observed_interval': (3, 3, 2),
                   'observed_drop_positions_zero_based': (2,),
                   'observed_drop_count': 1}
        and samples[-1] - samples[0] == -3)
    checks['nondecreasing_observations_telescope'] = all(
        observed_increase(values)['minimum_compatible_events'] == values[-1] - values[0]
        for values in ((0, 1), (4, 4, 9), (0, 12, 32, 64)))
    checks['constant_including_zero_has_no_observed_events_or_drops'] = all(
        observed_increase((value,) * 4)['minimum_compatible_events'] == 0
        and observed_increase((value,) * 4)['observed_drop_count'] == 0
        for value in (0, 7, 64))
    checks['drop_to_zero_counts_a_drop_but_zero_minimum_events'] = (
        observed_increase((8, 0))['minimum_compatible_events'] == 0
        and observed_increase((8, 0))['observed_drop_count'] == 1)
    checks['first_observation_is_not_an_in_window_event_count'] = (
        observed_increase((40, 42))['minimum_compatible_events'] == 2)

    rows = [graph_costs(start, MAX_SAMPLE) for start in range(MAX_SAMPLE + 1)]
    graph_pairs = 0
    graph_ok = True
    for first, last in itertools.product(range(MAX_SAMPLE + 1), repeat=2):
        graph_ok = graph_ok and (
            observed_increase((first, last))['minimum_compatible_events'] == rows[first][last])
        graph_pairs += 1
    checks['all_finite_pairs_match_independent_state_graph_ONE_family'] = bool(graph_ok)

    split_cases = 0
    split_ok = True
    for values in ((0, 6, 2), samples, (8, 0, 7, 1, 9), (64, 1, 0, 64)):
        for middle in range(1, len(values) - 1):
            split_ok = split_ok and (
                observed_increase(values)['minimum_compatible_events']
                == observed_increase(values[:middle + 1])['minimum_compatible_events']
                + observed_increase(values[middle:])['minimum_compatible_events'])
            split_cases += 1
    checks['shared_boundary_observation_adds_no_duplicate_baseline'] = bool(split_ok)

    direct = replay_history(5, (('increment', 2),))
    hidden = replay_history(5, (('reset', 0), ('increment', 7)))
    repeated = replay_history(5, (('reset', 0), ('increment', 7),
                                  ('reset', 0), ('increment', 7)))
    checks['same_increasing_observations_hide_different_events_and_reset_counts'] = (
        direct['last_observation'] == hidden['last_observation'] == repeated['last_observation'] == 7
        and [x['actual_witness_events'] for x in (direct, hidden, repeated)] == [2, 7, 14]
        and [x['actual_witness_resets'] for x in (direct, hidden, repeated)] == [0, 1, 2]
        and observed_increase((5, 7))['minimum_compatible_events'] == 2
        and observed_increase((5, 7))['observed_drop_count'] == 0)
    minimal_drop = replay_history(8, (('reset', 0), ('increment', 3)))
    extra_before_drop = replay_history(8, (('increment', 2), ('reset', 0), ('increment', 3)))
    checks['events_before_an_observed_drop_can_also_be_hidden'] = (
        minimal_drop['last_observation'] == extra_before_drop['last_observation'] == 3
        and minimal_drop['actual_witness_events'] == 3
        and extra_before_drop['actual_witness_events'] == 5
        and observed_increase((8, 3))['minimum_compatible_events'] == 3)

    a, b = (8, 3), (2, 7)
    per_identity = sum(observed_increase(x)['minimum_compatible_events'] for x in (a, b))
    merged_samples = tuple(x + y for x, y in zip(a, b, strict=True))
    premature = observed_increase(merged_samples)['minimum_compatible_events']
    checks['adding_snapshots_first_can_hide_a_source_reset'] = (
        merged_samples == (10, 10) and per_identity == 8 and premature == 0)

    thinning_cases = 0
    thinning_ok = True
    for first, middle, last in itertools.product(range(9), repeat=3):
        thinning_ok = thinning_ok and (
            observed_increase((first, last))['minimum_compatible_events']
            <= observed_increase((first, middle, last))['minimum_compatible_events'])
        thinning_cases += 1
    checks['discarding_an_observation_cannot_raise_minimum_ONE_family'] = bool(thinning_ok)

    class IntSubclass(int):
        pass

    class TupleSubclass(tuple):
        pass

    type_bad = ([1, 2], iter((1, 2)), TupleSubclass((1, 2)),
                (False, 2), (1, True), (1.0, 2), ('1', 2), (IntSubclass(1), 2))
    value_bad = ((), (1,), (0,) * 17, (-1, 2), (1, 65))
    checks['own_profile_rejects_types_bounds_and_preserves_input'] = (
        all(raises(lambda value=value: observed_increase(value), TypeError) for value in type_bad)
        and all(raises(lambda value=value: observed_increase(value), ValueError) for value in value_bad)
        and samples is original and samples == (8, 11, 3, 5)
        and observed_increase((0, 64))['minimum_compatible_events'] == 64)
    assert all(type(value) is bool and value for value in checks.values()), checks
    return {
        'python': platform.python_version(),
        'implementation': platform.python_implementation(),
        'scope': 'own finite synthetic observations; +1 events/free reset-to-zero only',
        'profile': {'exact_tuple_length': [2, 16], 'exact_int_observations': [0, 64]},
        'samples': samples,
        'naive_last_minus_first': samples[-1] - samples[0],
        'observed_result': result,
        'same_samples_5_to_7_witness_histories': [direct, hidden, repeated],
        'same_samples_8_to_3_witness_histories': [minimal_drop, extra_before_drop],
        'aggregation_witness': {'a': a, 'b': b, 'sum_of_per_identity_minimum_events': per_identity,
                                'premature_added_samples': merged_samples,
                                'premature_minimum_events': premature},
        'independent_state_graph_pairs_inside_ONE_family': graph_pairs,
        'shared_boundary_split_cases_inside_ONE_family': split_cases,
        'observation_thinning_cases_inside_ONE_family': thinning_cases,
        'timestamps_rate_extrapolation_or_prometheus_executed': False,
        'actual_service_or_network_measurement': False,
        'checks': checks,
    }


if __name__ == '__main__':
    print(json.dumps(run_example(), indent=2))

مرجعِ مستقل، تفاضلِ دو مقدار را با شاخهٔ اصلی حساب نمی‌کند. یک گرافِ متناهی از حالت‌های صفر تا ۶۴ می‌سازد: یالِ افزایشِ یک‌واحدی هزینهٔ یک دارد و یالِ بازنشانی به صفر هزینهٔ صفر. جست‌وجوی کوتاه‌ترین مسیر، کمترین هزینهٔ رسیدن از هر مشاهده به بعدی را پیدا می‌کند. محدودکردنِ حالت‌های مرجع به این بازه، حداقل را از بین نمی‌برد؛ رفتن به بالاتر از هر دو انتها و سپس بازنشانی، فقط رخدادِ اضافی مصرف می‌کند.

هر ۴۲۲۵ زوجِ ممکن در یک گروه با این مرجع مقایسه شد. ۷۲۹ سه‌تاییِ کوچک نیز در یک گروه نشان دادند حذفِ یک مشاهده نمی‌تواند حداقلِ لازم را بیشتر کند؛ با حذفِ قید، مسیرهای مجاز کمتر نمی‌شوند. دوازده گروهِ نام‌دار گذشتند. اجرای نهایی با CPython 3.14.5 انجام شد و stderr خالی بود. یک اجرای قبلی با خروجِ صفر، برچسبِ دو فیکسچرِ گزارش را به‌سببِ هم‌نامیِ متغیرها نادرست چاپ کرده بود؛ پیش از اصلاح محفوظ ماند و نسخهٔ نهایی یک بار جدا اجرا شد. این دو اجرا، دو مجموعهٔ مستقلِ دوازده‌تایی محسوب نمی‌شوند.

{
  "python": "3.14.5",
  "implementation": "CPython",
  "scope": "own finite synthetic observations; +1 events/free reset-to-zero only",
  "profile": {
    "exact_tuple_length": [
      2,
      16
    ],
    "exact_int_observations": [
      0,
      64
    ]
  },
  "samples": [
    8,
    11,
    3,
    5
  ],
  "naive_last_minus_first": -3,
  "observed_result": {
    "minimum_compatible_events": 8,
    "per_observed_interval": [
      3,
      3,
      2
    ],
    "observed_drop_positions_zero_based": [
      2
    ],
    "observed_drop_count": 1
  },
  "same_samples_5_to_7_witness_histories": [
    {
      "last_observation": 7,
      "actual_witness_events": 2,
      "actual_witness_resets": 0
    },
    {
      "last_observation": 7,
      "actual_witness_events": 7,
      "actual_witness_resets": 1
    },
    {
      "last_observation": 7,
      "actual_witness_events": 14,
      "actual_witness_resets": 2
    }
  ],
  "same_samples_8_to_3_witness_histories": [
    {
      "last_observation": 3,
      "actual_witness_events": 3,
      "actual_witness_resets": 1
    },
    {
      "last_observation": 3,
      "actual_witness_events": 5,
      "actual_witness_resets": 1
    }
  ],
  "aggregation_witness": {
    "a": [
      8,
      3
    ],
    "b": [
      2,
      7
    ],
    "sum_of_per_identity_minimum_events": 8,
    "premature_added_samples": [
      10,
      10
    ],
    "premature_minimum_events": 0
  },
  "independent_state_graph_pairs_inside_ONE_family": 4225,
  "shared_boundary_split_cases_inside_ONE_family": 8,
  "observation_thinning_cases_inside_ONE_family": 729,
  "timestamps_rate_extrapolation_or_prometheus_executed": false,
  "actual_service_or_network_measurement": false,
  "checks": {
    "hand_fixture_eight_events_not_naive_minus_three": true,
    "nondecreasing_observations_telescope": true,
    "constant_including_zero_has_no_observed_events_or_drops": true,
    "drop_to_zero_counts_a_drop_but_zero_minimum_events": true,
    "first_observation_is_not_an_in_window_event_count": true,
    "all_finite_pairs_match_independent_state_graph_ONE_family": true,
    "shared_boundary_observation_adds_no_duplicate_baseline": true,
    "same_increasing_observations_hide_different_events_and_reset_counts": true,
    "events_before_an_observed_drop_can_also_be_hidden": true,
    "adding_snapshots_first_can_hide_a_source_reset": true,
    "discarding_an_observation_cannot_raise_minimum_ONE_family": true,
    "own_profile_rejects_types_bounds_and_preserves_input": true
  }
}

تابعِ increase به بازهٔ زمانی برون‌یابی می‌کند و ممکن است خروجیِ غیرصحیح بدهد؛ کدِ ما این رفتار را ندارد و هم‌ارزی با PromQL را ادعا نمی‌کند. منابع در ۲۸ مهر ۱۴۰۵ خوانده شده‌اند؛ تاریخِ انتشارِ اولیهٔ این صفحه‌ها معلوم نیست. درسِ صدکِ تأخیر و بازهٔ نرخِ خطا نیز قراردادهای مشاهدهٔ دیگری دارند؛ هیچ‌یک، اطلاعاتِ گمشدهٔ شمارنده را برنمی‌گرداند.

۵. چهار پرسش دربارهٔ شمار، بازنشانی و مشاهده

کمترین شمارِ سازگار، همهٔ تاریخچه را بازنمی‌گرداند
تصویر مفهومی: کمترین شمارِ سازگار، همهٔ تاریخچه را بازنمی‌گرداند؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پرسش اول: خروجیِ هشت یعنی دقیقاً هشت کار انجام شده؟ پاسخ: فقط حداقلِ سازگار با همین مشاهده‌ها و فرض‌های مدل است. افزایش‌های پیش از بازنشانی و بازنشانی‌های پنهان می‌توانند شمارِ واقعی را بیشتر کنند؛ ورودی راهِ تشخیصِ آن‌ها را ندارد.

پرسش دوم: افتِ دما یا طولِ صف را هم با همین تابع اصلاح کنیم؟ پاسخ: خیر؛ آن‌ها ممکن است واقعاً کم شوند. اعتبارِ نوع و دامنهٔ عددی، معنای Counter را ثابت نمی‌کند. پیش از تابع باید قراردادِ معیار و هویتِ منبع معلوم باشد.

پرسش سوم: آیا افت، علتِ راه‌اندازیِ مجدد را ثابت می‌کند؟ پاسخ: نه؛ تفسیرِ ما مشروط به مدلِ افزایش و بازنشانی است. جابه‌جاییِ هویت، دادهٔ خراب، ترتیبِ نادرست یا چرخشِ سخت‌افزاری خارج از این مدل‌اند و باید جدا بررسی شوند.

پرسش چهارم: نمونه‌های بیشتر همهٔ ابهام را رفع می‌کنند؟ پاسخ: مشاهدهٔ میانی می‌تواند کرانِ پایین را بالا ببرد، ولی تضمینِ دیدنِ تمامِ رخدادها نیست. برای ادعای شمارِ واقعی، شواهدِ اضافی مانند ثبتِ معتبرِ رخداد و مرزِ دوره لازم است؛ این درس چنین شواهدی تولید نمی‌کند.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۸ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.