Skip to content

Commit 2b11e13

Browse files
authored
feat: project telemetry config models (#1972)
* feat: project telemetry config models * next steps * db type * use consistent snake case * update models, bump typeshare * simpler model names * add discriminant db type * chore(admin): add back beta access cmds * nit: remove half-unused type alias
1 parent 82ddbce commit 2b11e13

File tree

10 files changed

+206
-15
lines changed

10 files changed

+206
-15
lines changed

.circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ jobs:
173173
- restore-cargo-and-sccache
174174
- install-cargo-make
175175
- run: cargo make ci-workspace
176-
- run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/1Password/typeshare/releases/download/v1.11.0/typeshare-cli-v1.11.0-installer.sh | sh
176+
- run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/1Password/typeshare/releases/download/v1.13.0/typeshare-cli-v1.13.0-installer.sh | sh
177177
- run: cargo make types
178178
- run: cargo make check-types
179179
- save-sccache

Makefile.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ command = "git-cliff"
109109
args = ["-o", "CHANGELOG.md", "-t", "${@}"]
110110

111111
[tasks.types]
112-
install_crate = { crate_name = "typeshare-cli", binary = "typeshare", test_arg = ["-V"], version = "1.11.0" }
112+
install_crate = { crate_name = "typeshare-cli", binary = "typeshare", test_arg = ["-V"], version = "1.13.0" }
113113
command = "typeshare"
114114
args = ["common", "-l", "typescript", "-c", "common/typeshare.toml", "-o", "common/types.ts"]
115115

admin/src/args.rs

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
use clap::{Parser, Subcommand};
2-
use shuttle_common::{
3-
constants::API_URL_DEFAULT_BETA,
4-
models::{project::ComputeTier, user::UserId},
5-
};
2+
use shuttle_common::{constants::API_URL_DEFAULT_BETA, models::project::ComputeTier};
63

74
#[derive(Parser, Debug)]
85
pub struct Args {
@@ -22,7 +19,7 @@ pub struct Args {
2219
pub enum Command {
2320
ChangeProjectOwner {
2421
project_name: String,
25-
new_user_id: UserId,
22+
new_user_id: String,
2623
},
2724

2825
UpdateCompute {
@@ -37,6 +34,13 @@ pub enum Command {
3734
/// Renew all old custom domain certificates
3835
RenewCerts,
3936

37+
SetBetaAccess {
38+
user_id: String,
39+
},
40+
UnsetBetaAccess {
41+
user_id: String,
42+
},
43+
4044
/// Garbage collect free tier projects
4145
Gc {
4246
/// days since last deployment to filter by

admin/src/client.rs

+19
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ impl Client {
4242
.await
4343
}
4444

45+
pub async fn set_beta_access(&self, user_id: &str, access: bool) -> Result<()> {
46+
let resp = if access {
47+
self.inner
48+
.put(format!("/admin/users/{user_id}/beta"), Option::<()>::None)
49+
.await?
50+
} else {
51+
self.inner
52+
.delete(format!("/admin/users/{user_id}/beta"), Option::<()>::None)
53+
.await?
54+
};
55+
56+
if !resp.status().is_success() {
57+
dbg!(resp);
58+
panic!("request failed");
59+
}
60+
61+
Ok(())
62+
}
63+
4564
pub async fn gc_free_tier(&self, days: u32) -> Result<Vec<String>> {
4665
let path = format!("/admin/gc/free/{days}");
4766
self.inner.get_json(&path).await

admin/src/lib.rs

+8
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ pub async fn run(args: Args) {
4040
.unwrap();
4141
println!("{res:?}");
4242
}
43+
Command::SetBetaAccess { user_id } => {
44+
client.set_beta_access(&user_id, true).await.unwrap();
45+
println!("Set user {user_id} beta access");
46+
}
47+
Command::UnsetBetaAccess { user_id } => {
48+
client.set_beta_access(&user_id, false).await.unwrap();
49+
println!("Unset user {user_id} beta access");
50+
}
4351
Command::Gc {
4452
days,
4553
stop_deployments,

common/src/models/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ pub mod log;
66
pub mod project;
77
pub mod resource;
88
pub mod team;
9+
pub mod telemetry;
910
pub mod user;

common/src/models/team.rs

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use serde::{Deserialize, Serialize};
22
use strum::{Display, EnumString};
33

4-
use super::user::UserId;
5-
64
/// Minimal team information
75
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
86
pub struct Response {
@@ -20,7 +18,7 @@ pub struct Response {
2018
#[derive(Debug, Serialize, Deserialize, PartialEq)]
2119
pub struct MemberResponse {
2220
/// User ID
23-
pub id: UserId,
21+
pub id: String,
2422

2523
/// Role of the user in the team
2624
pub role: MemberRole,

common/src/models/telemetry.rs

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
/// Status of a telemetry export configuration for an external sink
4+
#[derive(Eq, Clone, Debug, PartialEq, Serialize, Deserialize)]
5+
#[typeshare::typeshare]
6+
pub struct TelemetrySinkStatus {
7+
/// Indicates that the associated project is configured to export telemetry data to this sink
8+
enabled: bool,
9+
}
10+
11+
/// A safe-for-display representation of the current telemetry export configuration for a given project
12+
#[derive(Eq, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
13+
#[typeshare::typeshare]
14+
pub struct TelemetryConfigResponse {
15+
betterstack: Option<TelemetrySinkStatus>,
16+
datadog: Option<TelemetrySinkStatus>,
17+
grafana_cloud: Option<TelemetrySinkStatus>,
18+
}
19+
20+
impl From<Vec<TelemetrySinkConfig>> for TelemetryConfigResponse {
21+
fn from(value: Vec<TelemetrySinkConfig>) -> Self {
22+
let mut instance = Self::default();
23+
24+
for sink in value {
25+
match sink {
26+
TelemetrySinkConfig::Betterstack(_) => {
27+
instance.betterstack = Some(TelemetrySinkStatus { enabled: true })
28+
}
29+
TelemetrySinkConfig::Datadog(_) => {
30+
instance.datadog = Some(TelemetrySinkStatus { enabled: true })
31+
}
32+
TelemetrySinkConfig::GrafanaCloud(_) => {
33+
instance.grafana_cloud = Some(TelemetrySinkStatus { enabled: true })
34+
}
35+
}
36+
}
37+
38+
instance
39+
}
40+
}
41+
42+
/// The user-supplied config required to export telemetry to a given external sink
43+
#[derive(
44+
Eq, Clone, PartialEq, Serialize, Deserialize, strum::AsRefStr, strum::EnumDiscriminants,
45+
)]
46+
#[serde(tag = "type", content = "content", rename_all = "snake_case")]
47+
#[strum(serialize_all = "snake_case")]
48+
#[typeshare::typeshare]
49+
#[strum_discriminants(derive(Serialize, Deserialize, strum::AsRefStr))]
50+
#[strum_discriminants(serde(rename_all = "snake_case"))]
51+
#[strum_discriminants(strum(serialize_all = "snake_case"))]
52+
pub enum TelemetrySinkConfig {
53+
/// [Betterstack](https://betterstack.com/docs/logs/open-telemetry/)
54+
Betterstack(BetterstackConfig),
55+
/// [Datadog](https://docs.datadoghq.com/opentelemetry/collector_exporter/otel_collector_datadog_exporter)
56+
Datadog(DatadogConfig),
57+
/// [Grafana Cloud](https://grafana.com/docs/grafana-cloud/send-data/otlp/)
58+
GrafanaCloud(GrafanaCloudConfig),
59+
}
60+
61+
impl TelemetrySinkConfig {
62+
pub fn as_db_type(&self) -> String {
63+
format!("project::telemetry::{}::config", self.as_ref())
64+
}
65+
}
66+
67+
impl TelemetrySinkConfigDiscriminants {
68+
pub fn as_db_type(&self) -> String {
69+
format!("project::telemetry::{}::config", self.as_ref())
70+
}
71+
}
72+
73+
#[derive(Eq, Clone, PartialEq, Serialize, Deserialize)]
74+
#[typeshare::typeshare]
75+
pub struct BetterstackConfig {
76+
pub source_token: String,
77+
}
78+
#[derive(Eq, Clone, PartialEq, Serialize, Deserialize)]
79+
#[typeshare::typeshare]
80+
pub struct DatadogConfig {
81+
pub api_key: String,
82+
}
83+
#[derive(Eq, Clone, PartialEq, Serialize, Deserialize)]
84+
#[typeshare::typeshare]
85+
pub struct GrafanaCloudConfig {
86+
pub token: String,
87+
pub endpoint: String,
88+
pub instance_id: String,
89+
}
90+
91+
#[cfg(test)]
92+
mod tests {
93+
use super::*;
94+
95+
#[test]
96+
fn sink_config_enum() {
97+
assert_eq!(
98+
"betterstack",
99+
TelemetrySinkConfig::Betterstack(BetterstackConfig {
100+
source_token: "".into()
101+
})
102+
.as_ref()
103+
);
104+
assert_eq!(
105+
"project::telemetry::betterstack::config",
106+
TelemetrySinkConfig::Betterstack(BetterstackConfig {
107+
source_token: "".into()
108+
})
109+
.as_db_type()
110+
);
111+
112+
assert_eq!(
113+
"betterstack",
114+
TelemetrySinkConfigDiscriminants::Betterstack.as_ref()
115+
);
116+
assert_eq!(
117+
"grafana_cloud",
118+
TelemetrySinkConfigDiscriminants::GrafanaCloud.as_ref()
119+
);
120+
assert_eq!(
121+
"\"betterstack\"",
122+
serde_json::to_string(&TelemetrySinkConfigDiscriminants::Betterstack).unwrap()
123+
);
124+
assert_eq!(
125+
"\"grafana_cloud\"",
126+
serde_json::to_string(&TelemetrySinkConfigDiscriminants::GrafanaCloud).unwrap()
127+
);
128+
}
129+
}

common/src/models/user.rs

-4
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ use crossterm::style::Stylize;
77
#[cfg(feature = "display")]
88
use std::fmt::Write;
99

10-
/// In normal cases, a string with the format `user_<ULID>`.
11-
/// This is a soft rule and the string can be something different.
12-
pub type UserId = String;
13-
1410
#[derive(Deserialize, Serialize, Debug)]
1511
#[typeshare::typeshare]
1612
pub struct UserResponse {

common/types.ts

+37-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)