Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -490,8 +490,9 @@ jobs:
matrix:
version:
[
"25.8.16.10001.altinitystable",
"26.3.33.10001.altinitystable",
"altinity-clickhouse-server:25.8.16.10001.altinitystable",
"altinity-clickhouse-server:26.3.33.10001.altinitystable",
"clickhouse-clickhouse-server:26.8.10.6",
]
test_type: [snuba-test, snuba-test-migrations]

Expand Down Expand Up @@ -523,7 +524,7 @@ jobs:
docker load --input /tmp/docker-deps/deps.tar
fi
images=(
"ghcr.io/getsentry/image-mirror-altinity-clickhouse-server:${{ matrix.version }}"
"ghcr.io/getsentry/image-mirror-${{ matrix.version }}"
"ghcr.io/getsentry/image-mirror-confluentinc-cp-kafka:6.2.0"
"ghcr.io/getsentry/image-mirror-confluentinc-cp-zookeeper:6.2.0"
"ghcr.io/getsentry/docker-redis-cluster:7.0.10"
Expand All @@ -540,7 +541,7 @@ jobs:

- name: Docker Snuba Test other ClickHouse versions
run: |
export CLICKHOUSE_IMAGE=ghcr.io/getsentry/image-mirror-altinity-clickhouse-server:${{matrix.version}}
export CLICKHOUSE_IMAGE=ghcr.io/getsentry/image-mirror-${{matrix.version}}
SNUBA_IMAGE=ghcr.io/getsentry/snuba-ci:${{ github.event.pull_request.head.sha || github.sha }} SNUBA_SETTINGS=test docker compose -f docker-compose.gcb.yml run --rm ${{ matrix.test_type }}

image-smoke-test:
Expand Down
5 changes: 4 additions & 1 deletion rust_snuba/src/processors/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ use uuid::Uuid;
use sentry_arroyo::backends::kafka::types::KafkaPayload;

use crate::config::ProcessorConfig;
use crate::processors::utils::{enforce_standard_retention, StringToIntDatetime64};
use crate::processors::utils::{
enforce_standard_retention, serialize_datetime64_ms, StringToIntDatetime64,
};
use crate::types::{
InsertBatch, InsertOrReplacement, KafkaMessageMetadata, ReplacementData, RowData,
};
Expand Down Expand Up @@ -414,6 +416,7 @@ struct ErrorRow {
#[serde(rename = "flags.value")]
flags_value: Vec<String>,
timestamp: u32,
#[serde(serialize_with = "serialize_datetime64_ms")]
timestamp_ms: u64,
title: String,
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down
3 changes: 3 additions & 0 deletions rust_snuba/src/processors/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use uuid::Uuid;

use sentry_arroyo::backends::kafka::types::KafkaPayload;

use crate::processors::utils::serialize_opt_datetime64_us;
use crate::types::{InsertBatch, KafkaMessageMetadata};

pub fn process_message(
Expand Down Expand Up @@ -103,7 +104,9 @@ struct Function<'a> {
release: Option<&'a str>,
retention_days: u32,
timestamp: u64,
#[serde(serialize_with = "serialize_opt_datetime64_us")]
start_timestamp: Option<u64>,
#[serde(serialize_with = "serialize_opt_datetime64_us")]
end_timestamp: Option<u64>,
transaction_name: &'a str,
thread_id: String,
Expand Down
12 changes: 9 additions & 3 deletions rust_snuba/src/processors/profile_chunks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use sentry_arroyo::backends::kafka::types::KafkaPayload;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::processors::utils::enforce_standard_retention;
use crate::processors::utils::{enforce_standard_retention, serialize_datetime64_us};
use crate::types::{InsertBatch, KafkaMessageMetadata};

pub fn process_message(
Expand Down Expand Up @@ -43,9 +43,15 @@ struct Chunk {
project_id: u64,
profiler_id: Uuid,
chunk_id: Uuid,
#[serde(rename(serialize = "start_timestamp"))]
#[serde(
rename(serialize = "start_timestamp"),
serialize_with = "serialize_datetime64_us"
)]
start_timestamp_micro: u64,
#[serde(rename(serialize = "end_timestamp"))]
#[serde(
rename(serialize = "end_timestamp"),
serialize_with = "serialize_datetime64_us"
)]
end_timestamp_micro: u64,
environment: Option<String>,
retention_days: Option<u16>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ expression: snapshot_payload
"id:b7478c1e-a9ba-4e5a-bc65-364142284f89"
],
"timestamp": 1677512412,
"timestamp_ms": 1677512412223,
"timestamp_ms": "1677512412.223",
"title": "Exception: Some exception.",
"trace_id": "1da660e8-8c01-430b-85fe-68537ed5838b",
"transaction_name": "MainActivity.capture_exception",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ expression: snapshot_payload
"id:b7478c1e-a9ba-4e5a-bc65-364142284f89"
],
"timestamp": 1677512412,
"timestamp_ms": 1677512412223,
"timestamp_ms": "1677512412.223",
"title": "Exception: Some exception.",
"trace_id": "1da660e8-8c01-430b-85fe-68537ed5838b",
"transaction_name": "MainActivity.capture_exception",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ expression: snapshot_payload
"id:b7478c1e-a9ba-4e5a-bc65-364142284f89"
],
"timestamp": 1677512412,
"timestamp_ms": 1677512412223,
"timestamp_ms": "1677512412.223",
"title": "Exception: Some exception.",
"trace_id": "1da660e8-8c01-430b-85fe-68537ed5838b",
"transaction_name": "MainActivity.capture_exception",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ expression: snapshot_payload
"tags.key": [],
"tags.value": [],
"timestamp": 1677512412,
"timestamp_ms": 1677512412223,
"timestamp_ms": "1677512412.223",
"title": "",
"transaction_name": "",
"type": "error",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ expression: snapshot_payload
"tags.key": [],
"tags.value": [],
"timestamp": 1678209849,
"timestamp_ms": 1678209849987,
"timestamp_ms": "1678209849.987",
"title": "",
"transaction_name": "",
"type": "error",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ expression: snapshot_payload
"tags.key": [],
"tags.value": [],
"timestamp": 1677512412,
"timestamp_ms": 1677512412223,
"timestamp_ms": "1677512412.223",
"title": "",
"transaction_name": "",
"type": "error",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ expression: snapshot_payload
"tags.key": [],
"tags.value": [],
"timestamp": 1679315796,
"timestamp_ms": 1679315796051,
"timestamp_ms": "1679315796.051",
"title": "redacted",
"transaction_name": "",
"type": "error",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ expression: snapshot_payload
"tags.key": [],
"tags.value": [],
"timestamp": 1678728889,
"timestamp_ms": 1678728889328,
"timestamp_ms": "1678728889.328",
"title": "",
"transaction_name": "",
"type": "error",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ expression: snapshot_payload
[
{
"chunk_id": "0432a0a4-c25f-4697-bf9f-0a2fcbe6a814",
"end_timestamp": 1710805689123456,
"end_timestamp": "1710805689.123456",
"environment": null,
"offset": 1,
"partition": 0,
"profiler_id": "4d229f1d-3807-421b-a62a-5f8bc295d836",
"project_id": 1,
"retention_days": 30,
"start_timestamp": 1710805688123456
"start_timestamp": "1710805688.123456"
}
]
45 changes: 44 additions & 1 deletion rust_snuba/src/processors/utils.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use chrono::{DateTime, NaiveDateTime, Utc};
use schemars::JsonSchema;
use sentry_options::options;
use serde::{Deserialize, Deserializer, Serialize};
use serde::{Deserialize, Deserializer, Serialize, Serializer};

// Equivalent to "%Y-%m-%dT%H:%M:%S.%fZ" in python
// Notice the differennce of .%fZ vs %.fZ, this comes from a difference in how rust's chrono handles the format
Expand Down Expand Up @@ -117,6 +117,41 @@ pub struct StringToIntDatetime(
pub u32,
);

/// Formats a DateTime64 tick count (at `precision` fractional digits) as a
/// quoted `"<seconds>.<fraction>"` string for JSONEachRow inserts.
///
/// Since ClickHouse 26.8, an unquoted JSON number fed to a DateTime64 column
/// is read as seconds instead of raw ticks
/// (https://github.com/ClickHouse/ClickHouse/pull/108091). A quoted decimal
/// seconds string is parsed identically by old and new versions.
pub fn format_datetime64(ticks: u64, precision: u32) -> String {
let scale = 10u64.pow(precision);
format!(
"{}.{:0width$}",
ticks / scale,
ticks % scale,
width = precision as usize
)
}

pub fn serialize_datetime64_ms<S: Serializer>(ticks: &u64, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&format_datetime64(*ticks, 3))
}

pub fn serialize_datetime64_us<S: Serializer>(ticks: &u64, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&format_datetime64(*ticks, 6))
}

pub fn serialize_opt_datetime64_us<S: Serializer>(
ticks: &Option<u64>,
s: S,
) -> Result<S::Ok, S::Error> {
match ticks {
Some(t) => serialize_datetime64_us(t, s),
None => s.serialize_none(),
}
}

#[derive(Debug, Deserialize, JsonSchema, Default, Serialize)]
pub struct StringToIntDatetime64(
#[serde(deserialize_with = "ensure_valid_datetime_64")]
Expand Down Expand Up @@ -159,6 +194,14 @@ mod tests {
assert_eq!(enforce_standard_retention(Some(100)), 90);
}

#[test]
fn test_format_datetime64() {
assert_eq!(format_datetime64(1677512412223, 3), "1677512412.223");
assert_eq!(format_datetime64(1677512412005, 3), "1677512412.005");
assert_eq!(format_datetime64(1710805688000012, 6), "1710805688.000012");
assert_eq!(format_datetime64(0, 3), "0.000");
}

#[test]
fn test_downsampled_defaults() {
init_options();
Expand Down
10 changes: 6 additions & 4 deletions snuba/datasets/processors/search_issues_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,13 @@ def _process_timestamp_ms(
self, event_data: IssueEventData, processed: MutableMapping[str, Any]
) -> None:
client_timestamp = processed["client_timestamp"]
# NOTE: we do this conversion because the JSONRowEncoder will strip out milliseconds out
# of datetime objects specifically. To work around that, we convert the datetime to a
# timestamp in milliseconds
# NOTE: the JSONRowEncoder strips milliseconds from datetime objects, so we send a
# quoted "<seconds>.<millis>" string instead. It must be quoted: since ClickHouse 26.8
# an unquoted number for a DateTime64 column is read as seconds, not raw ticks
# (https://github.com/ClickHouse/ClickHouse/pull/108091).
client_timestamp = client_timestamp.replace(tzinfo=UTC)
processed["timestamp_ms"] = int(client_timestamp.timestamp() * 1000)
ms = int(client_timestamp.timestamp() * 1000)
processed["timestamp_ms"] = f"{ms // 1000}.{ms % 1000:03d}"

def process_insert_v1(
self, event: SearchIssueEvent, metadata: KafkaMessageMetadata
Expand Down
5 changes: 4 additions & 1 deletion snuba/web/rpc/storage_routing/load_retriever.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import inspect
import json
import math
from collections.abc import Callable
from dataclasses import dataclass, fields
from functools import wraps
Expand Down Expand Up @@ -36,11 +37,13 @@ def to_dict(self) -> dict[str, float]:

@classmethod
def from_dict(cls, load_info_dict: dict[str, float | int | None]) -> LoadInfo:
# Missing and NaN values fall back to the -1 default. ClickHouse 26.8+ can
# return NaN instead of NULL for an aggregate with no matching metric rows.
return cls(
**{
f.name: float(v)
for f in fields(cls)
if (v := load_info_dict.get(f.name)) is not None
if (v := load_info_dict.get(f.name)) is not None and not math.isnan(v)
}
)

Expand Down
21 changes: 19 additions & 2 deletions tests/admin/clickhouse/test_copy_tables.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import re
from unittest.mock import MagicMock, patch

import pytest
Expand All @@ -24,6 +25,16 @@
from snuba.migrations.groups import MigrationGroup
from snuba.state.sentry_options import SNUBA_OPTIONS_NAMESPACE


def _normalize_statement(statement: str) -> str:
"""
ClickHouse 26.8+ wraps single-expression PARTITION BY keys in parentheses
in SHOW CREATE TABLE output, e.g. `PARTITION BY (toStartOfMonth(timestamp))`.
Strip them so expectations hold across versions.
"""
return re.sub(r"^PARTITION BY \((.*)\)$", r"PARTITION BY \1", statement, flags=re.MULTILINE)


OUTCOMES_DAILY_TABLE_NO_CLUSTER = """
CREATE TABLE IF NOT EXISTS {db}.outcomes_daily_local_v2
(
Expand Down Expand Up @@ -144,7 +155,10 @@ def test_get_table_statements(
)
ts = table_statements[0]
assert ts.is_mergetree == is_mergetree
assert ts.statement == statement.format(db=database_name, engine=engine).strip()
assert (
_normalize_statement(ts.statement)
== statement.format(db=database_name, engine=engine).strip()
)


@pytest.mark.redis_db
Expand Down Expand Up @@ -181,7 +195,10 @@ def test_get_table_statement_without_cluster() -> None:
)
ts = table_statements[0]
assert ts.is_mergetree
assert ts.statement == statement.format(db=database_name, engine=engine).strip()
assert (
_normalize_statement(ts.statement)
== statement.format(db=database_name, engine=engine).strip()
)


@pytest.mark.redis_db
Expand Down
3 changes: 2 additions & 1 deletion tests/datasets/test_errors_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,8 @@ def build_result(self, meta: KafkaMessageMetadata) -> MutableMapping[str, Any]:
"partition": meta.partition,
"offset": meta.offset,
"message_timestamp": int(self.timestamp.replace(tzinfo=UTC).timestamp()),
"timestamp_ms": int(self.timestamp.replace(tzinfo=UTC).timestamp() * 1000),
"timestamp_ms": f"{int(self.timestamp.replace(tzinfo=UTC).timestamp())}"
f".{self.timestamp.microsecond // 1000:03d}",
"retention_days": 58,
"deleted": 0,
"group_id": self.group_id,
Expand Down
3 changes: 2 additions & 1 deletion tests/datasets/test_search_issues_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ def test_extract_timestamp_ms(self, message_base):
self.assert_required_columns(processed)
insert_row = processed.rows[0]
client_timestamp_utc = insert_row["client_timestamp"].replace(tzinfo=UTC)
assert insert_row["timestamp_ms"] == int(client_timestamp_utc.timestamp() * 1000)
ms = int(client_timestamp_utc.timestamp() * 1000)
assert insert_row["timestamp_ms"] == f"{ms // 1000}.{ms % 1000:03d}"

def test_extract_user(self, message_base):
message_with_user = message_base
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,12 @@ def test_formula_reliability_with_group_by(self) -> None:
group_by=[
AttributeKey(type=AttributeKey.TYPE_STRING, name="myattr"),
],
order_by=[
TraceItemTableRequest.OrderBy(
column=Column(key=AttributeKey(type=AttributeKey.TYPE_STRING, name="myattr")),
descending=True,
),
],
limit=10,
)
response = EndpointTraceItemTable().execute(message)
Expand Down
Loading