Skip to main content

stackless_integrations/providers/pydantic/
logfire.rs

1//! `pydantic/logfire` integration.
2
3use std::collections::BTreeMap;
4
5use serde::Serialize;
6use stackless_stripe_projects::catalog::verify::CatalogService;
7use stackless_stripe_projects::provision::ProvisionContext;
8
9use super::FamilyResource;
10use crate::error::IntegrationError;
11use crate::hostable::{ConfigScope, Hostable, IntegrationHosting};
12
13pub const RESOURCE_KIND: &str = "integration-pydantic";
14
15#[derive(Debug, Serialize)]
16pub struct PydanticLogfireConfig {
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub organization: Option<String>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub project_name: Option<String>,
21}
22
23impl CatalogService for PydanticLogfireConfig {
24    const REFERENCE: &'static str = "pydantic/logfire";
25}
26
27#[derive(Debug)]
28pub struct PydanticLogfire;
29
30impl Hostable for PydanticLogfire {
31    const PROVIDER: &'static str = "pydantic";
32    const HOSTING: IntegrationHosting = IntegrationHosting::Managed;
33    const CONFIG_SCOPE: ConfigScope = ConfigScope::GlobalOnly;
34    const RESOURCE_KIND: &'static str = RESOURCE_KIND;
35    const OUTPUTS: &'static [&'static str] = &[
36        "logfire_base_url",
37        "logfire_organization_id",
38        "logfire_organization_name",
39        "logfire_project_id",
40        "logfire_project_name",
41        "logfire_token",
42    ];
43}
44
45impl FamilyResource for PydanticLogfire {
46    type Config = PydanticLogfireConfig;
47    const PROVIDER_PREFIX: &'static str = "PYDANTIC";
48    const OUTPUT_FIELDS: &'static [(&'static str, &'static str, bool)] = &[
49        ("LOGFIRE_BASE_URL", "logfire_base_url", true),
50        ("LOGFIRE_ORGANIZATION_ID", "logfire_organization_id", true),
51        (
52            "LOGFIRE_ORGANIZATION_NAME",
53            "logfire_organization_name",
54            true,
55        ),
56        ("LOGFIRE_PROJECT_ID", "logfire_project_id", true),
57        ("LOGFIRE_PROJECT_NAME", "logfire_project_name", true),
58        ("LOGFIRE_TOKEN", "logfire_token", true),
59    ];
60
61    fn build_config(ctx: &ProvisionContext<'_>) -> Result<PydanticLogfireConfig, IntegrationError> {
62        let config = super::integration_config(ctx)?;
63        Ok(PydanticLogfireConfig {
64            organization: super::interp_optional(ctx, &config, "organization")?,
65            project_name: super::interp_optional(ctx, &config, "project_name")?,
66        })
67    }
68}
69
70pub fn validate_config(
71    name: &str,
72    config: &BTreeMap<String, toml::Value>,
73) -> Result<(), IntegrationError> {
74    let _ = (name, config);
75    Ok(())
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::ProviderOps;
82    use crate::resource::ResourcePayload;
83    use stackless_core::def::StackDef;
84    use stackless_stripe_projects::stripe::StripeProjects;
85    use stackless_stripe_projects::test_support;
86
87    #[test]
88    fn config_matches_catalog() {
89        const FIXTURE: &str = include_str!(concat!(
90            env!("CARGO_MANIFEST_DIR"),
91            "/../stackless-stripe-projects/tests/fixtures/catalog.json"
92        ));
93        let catalog = stackless_stripe_projects::Catalog::from_json_envelope(FIXTURE).unwrap();
94        let failures = stackless_stripe_projects::verify_service(
95            &catalog,
96            &PydanticLogfireConfig {
97                organization: None,
98                project_name: None,
99            },
100        );
101        assert!(
102            failures.is_empty(),
103            "pydantic/logfire catalog gaps:\n{}",
104            failures.join("\n")
105        );
106    }
107
108    const CATALOG_ENVELOPE: &str = r##"{"ok":true,"command":"projects catalog","data":{"last_updated":"2026-07-11T00:00:00Z","services":[{"id":"prvsvc_logfire","object":"v2.provisioning.provider_service_detail","provider_id":"prvdr_pydantic","provider_name":"Pydantic","service_id":"logfire","categories":["database"],"kind":"deployable","scope":"project","availability":"available","development":false,"livemode":true,"pricing":{"type":"component"},"configuration_schema":{"properties":{"organization":{"type":"string"},"project_name":{"type":"string"}},"type":"object"}}]}}"##;
109
110    fn test_def() -> StackDef {
111        StackDef::parse(
112            r#"
113[stack]
114name = "atto"
115[stack.projects.stripe]
116project = "project_1"
117[integrations.res]
118provider = "pydantic"
119[services.api]
120source = { repo = "r", ref = "main" }
121env = { OUT = "${integrations.res.logfire_token}" }
122health = { path = "/health" }
123[services.api.local]
124run = "true"
125"#,
126        )
127        .unwrap()
128    }
129
130    #[tokio::test]
131    async fn provision_records_outputs() {
132        let runner = test_support::provision_script(
133            CATALOG_ENVELOPE,
134            serde_json::json!({"PYDANTIC_LOGFIRE_BASE_URL": "val_logfire_base_url", "PYDANTIC_LOGFIRE_ORGANIZATION_ID": "val_logfire_organization_id", "PYDANTIC_LOGFIRE_ORGANIZATION_NAME": "val_logfire_organization_name", "PYDANTIC_LOGFIRE_PROJECT_ID": "val_logfire_project_id", "PYDANTIC_LOGFIRE_PROJECT_NAME": "val_logfire_project_name", "PYDANTIC_LOGFIRE_TOKEN": "val_logfire_token"}),
135            0,
136        );
137        let dir = tempfile::tempdir().unwrap();
138        std::fs::write(
139            dir.path().join("stackless.toml"),
140            "[stack]\nname=\"atto\"\n",
141        )
142        .unwrap();
143        let stripe = StripeProjects::new(&runner, dir.path());
144
145        let resource = PydanticLogfire
146            .provision(
147                &stripe.as_dyn(),
148                &test_def(),
149                dir.path(),
150                "demo",
151                "res",
152                "local",
153                false,
154            )
155            .await
156            .unwrap();
157        assert_eq!(resource.resource_kind, "integration-pydantic");
158        let payload: ResourcePayload = serde_json::from_str(&resource.payload).unwrap();
159        assert_eq!(payload.outputs["logfire_token"], "val_logfire_token");
160    }
161}