mz_adapter/
telemetry.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Telemetry utilities.
11
12use chrono::{DateTime, Utc};
13use mz_audit_log::ObjectType;
14use mz_sql::catalog::EnvironmentId;
15use mz_sql_parser::ast::StatementKind;
16use serde::{Deserialize, Serialize};
17use serde_json::json;
18use uuid::Uuid;
19
20/// Details to attach to a Segment event.
21#[derive(Debug, Clone, Default)]
22pub struct EventDetails<'a> {
23    /// The ID of the user that triggered the event, if any.
24    pub user_id: Option<Uuid>,
25    /// The value of the `application_name` parameter for the session that
26    /// triggered the event, if any.
27    pub application_name: Option<&'a str>,
28    /// The timestamp at which the event occurred, if not the current time.
29    pub timestamp: Option<DateTime<Utc>>,
30}
31
32/// Extension trait for [`mz_segment::Client`].
33pub trait SegmentClientExt {
34    /// Tracks an event associated with an environment.
35    fn environment_track<S>(
36        &self,
37        environment_id: &EnvironmentId,
38        event: S,
39        properties: serde_json::Value,
40        details: EventDetails<'_>,
41    ) where
42        S: Into<String>;
43}
44
45impl SegmentClientExt for mz_segment::Client {
46    /// Tracks an event associated with an environment.
47    ///
48    /// Various metadata about the environment is automatically attached
49    /// using canonical field names.
50    ///
51    /// # Panics
52    ///
53    /// Panics if `properties` is not a [`serde_json::Value::Object`].
54    fn environment_track<S>(
55        &self,
56        environment_id: &EnvironmentId,
57        event: S,
58        mut properties: serde_json::Value,
59        EventDetails {
60            user_id,
61            timestamp,
62            application_name,
63        }: EventDetails<'_>,
64    ) where
65        S: Into<String>,
66    {
67        {
68            let properties = match &mut properties {
69                serde_json::Value::Object(map) => map,
70                _ => {
71                    panic!("SegmentClientExt::environment_track called with non-object properties")
72                }
73            };
74            properties.insert("event_source".into(), json!("environment"));
75            properties.insert(
76                "cloud_provider".into(),
77                json!(environment_id.cloud_provider().to_string()),
78            );
79            properties.insert(
80                "cloud_provider_region".into(),
81                json!(environment_id.cloud_provider_region()),
82            );
83            if let Some(application_name) = application_name {
84                properties.insert("application_name".into(), json!(application_name));
85            }
86        }
87
88        // "Context" is a defined dictionary of extra information related to a datapoint. Please
89        // consult the docs before adding anything here: https://segment.com/docs/connections/spec/common/#context
90        let context = json!({
91            "group_id": environment_id.organization_id()
92        });
93
94        // We use the organization ID as the user ID for events that are not
95        // associated with a particular user.
96        let user_id = user_id.unwrap_or_else(|| environment_id.organization_id());
97
98        self.track(user_id, event, properties, Some(context), timestamp);
99    }
100}
101
102/// Describes a way in which DDL statement execution can fail.
103#[derive(Clone, Debug, Serialize, Deserialize)]
104#[serde(rename_all = "kebab-case")]
105pub enum StatementFailureType {
106    /// The statement failed to parse.
107    ParseFailure,
108}
109
110impl StatementFailureType {
111    /// Renders the name of failure type as a title case string.
112    pub fn as_title_case(&self) -> &'static str {
113        match self {
114            StatementFailureType::ParseFailure => "Parse Failed",
115        }
116    }
117}
118
119serde_plain::derive_display_from_serialize!(StatementFailureType);
120
121/// Describes the action of a DDL statement.
122#[derive(Serialize, Deserialize)]
123pub enum StatementAction {
124    /// The statement alters an object.
125    Alter,
126    /// The statement creates an object.
127    Create,
128}
129
130impl StatementAction {
131    /// Renders the DDL action as a title case string.
132    pub fn as_title_case(&self) -> &'static str {
133        match self {
134            StatementAction::Alter => "Alter",
135            StatementAction::Create => "Create",
136        }
137    }
138}
139
140/// Analyzes the action and object type of DDL statement kinds.
141///
142/// Returns `None` if the given statement kind is not a tracked DDL `StatementKind`. Tracked
143/// statements are those which are written to the audit log.
144pub fn analyze_audited_statement(
145    statement: StatementKind,
146) -> Option<(StatementAction, ObjectType)> {
147    match statement {
148        StatementKind::AlterConnection => Some((StatementAction::Alter, ObjectType::Connection)),
149        StatementKind::AlterIndex => Some((StatementAction::Alter, ObjectType::Index)),
150        StatementKind::AlterRole => Some((StatementAction::Alter, ObjectType::Role)),
151        StatementKind::AlterSecret => Some((StatementAction::Alter, ObjectType::Secret)),
152        StatementKind::AlterSource => Some((StatementAction::Alter, ObjectType::Source)),
153        StatementKind::CreateCluster => Some((StatementAction::Create, ObjectType::Cluster)),
154        StatementKind::CreateClusterReplica => {
155            Some((StatementAction::Create, ObjectType::ClusterReplica))
156        }
157        StatementKind::CreateConnection => Some((StatementAction::Create, ObjectType::Connection)),
158        StatementKind::CreateDatabase => Some((StatementAction::Create, ObjectType::Database)),
159        StatementKind::CreateIndex => Some((StatementAction::Create, ObjectType::Index)),
160        StatementKind::CreateMaterializedView => {
161            Some((StatementAction::Create, ObjectType::MaterializedView))
162        }
163        StatementKind::CreateRole => Some((StatementAction::Create, ObjectType::Role)),
164        StatementKind::CreateSchema => Some((StatementAction::Create, ObjectType::Schema)),
165        StatementKind::CreateSecret => Some((StatementAction::Create, ObjectType::Secret)),
166        StatementKind::CreateSink => Some((StatementAction::Create, ObjectType::Sink)),
167        StatementKind::CreateSource => Some((StatementAction::Create, ObjectType::Source)),
168        StatementKind::CreateTable => Some((StatementAction::Create, ObjectType::Table)),
169        StatementKind::CreateView => Some((StatementAction::Create, ObjectType::View)),
170        _ => None,
171    }
172}