Packages
caffeine_lang
4.7.6
6.3.1
6.3.0
6.2.2
6.2.1
6.2.0
6.1.2
6.1.1
6.1.0
6.0.0
5.6.0
5.5.0
5.4.4
5.4.3
5.4.2
5.4.1
5.4.0
5.3.0
5.2.0
5.1.1
5.1.0
5.0.12
5.0.11
5.0.10
5.0.8
5.0.7
5.0.6
5.0.5
5.0.4
5.0.1
5.0.0
4.10.0
4.9.0
4.8.3
4.8.2
4.8.1
4.8.0
4.7.9
4.7.8
4.7.7
4.7.6
4.7.5
4.6.7
4.6.6
4.6.5
4.6.4
4.6.3
4.6.2
4.6.0
4.5.1
4.5.0
4.4.4
4.4.3
4.4.1
4.4.0
4.3.7
4.3.6
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.0.2
1.0.1
0.1.0
0.0.24
0.0.23
0.0.22
0.0.21
0.0.20
0.0.19
0.0.18
0.0.17
0.0.16
0.0.15
0.0.14
0.0.13
0.0.12
0.0.11
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.4
0.0.2
0.0.1
A compiler for generating reliability artifacts from service expectation definitions.
Current section
Files
Jump to
Current section
Files
src/caffeine_lang/codegen/datadog.gleam
import caffeine_lang/analysis/templatizer
import caffeine_lang/codegen/generator_utils
import caffeine_lang/constants
import caffeine_lang/errors.{type CompilationError}
import caffeine_lang/helpers
import caffeine_lang/linker/artifacts
import caffeine_lang/linker/ir.{
type DepsValidated, type IntermediateRepresentation, type Resolved,
IntermediateRepresentation, SloFields, ir_to_identifier,
}
import caffeine_lang/types.{
CollectionType, Dict, PrimitiveType, String as StringType,
}
import caffeine_lang/value
import caffeine_query_language/generator as cql_generator
import gleam/dict
import gleam/int
import gleam/list
import gleam/option
import gleam/result
import gleam/set
import gleam/string
import terra_madre/common
import terra_madre/hcl
import terra_madre/terraform
/// Resolves Datadog indicator templates in an intermediate representation.
@internal
pub fn resolve_indicators(
ir: IntermediateRepresentation(DepsValidated),
) -> Result(IntermediateRepresentation(Resolved), CompilationError) {
use indicators_value_tuple <- result.try(
ir.values
|> list.find(fn(vt) { vt.label == "indicators" })
|> result.replace_error(
errors.semantic_analysis_template_resolution_error(
msg: "expectation '"
<> ir_to_identifier(ir)
<> "' - missing 'indicators' field in IR",
),
),
)
use indicators_dict <- result.try(
value.extract_string_dict(indicators_value_tuple.value)
|> result.map_error(fn(_) {
errors.semantic_analysis_template_resolution_error(
msg: "expectation '"
<> ir_to_identifier(ir)
<> "' - failed to decode indicators",
)
}),
)
let identifier = ir_to_identifier(ir)
// Resolve all indicators and collect results.
use resolved_indicators <- result.try(
indicators_dict
|> dict.to_list
|> list.try_map(fn(pair) {
let #(key, indicator) = pair
use resolved <- result.map(
templatizer.parse_and_resolve_query_template(
indicator,
ir.values,
from: identifier,
),
)
#(key, resolved)
}),
)
// Build the new indicators dict as a Value.
let resolved_indicators_value =
resolved_indicators
|> list.map(fn(pair) { #(pair.0, value.StringValue(pair.1)) })
|> dict.from_list
|> value.DictValue
// Create the new indicators ValueTuple.
let new_indicators_value_tuple =
helpers.ValueTuple(
"indicators",
CollectionType(Dict(
PrimitiveType(StringType),
PrimitiveType(StringType),
)),
resolved_indicators_value,
)
// Also resolve templates in the "evaluation" field if present.
let evaluation_tuple_result =
ir.values
|> list.find(fn(vt) { vt.label == "evaluation" })
use resolved_evaluation_tuple <- result.try(case evaluation_tuple_result {
Error(_) -> Ok(option.None)
Ok(evaluation_tuple) -> {
use evaluation_string <- result.try(
value.extract_string(evaluation_tuple.value)
|> result.map_error(fn(_) {
errors.semantic_analysis_template_resolution_error(
msg: "expectation '"
<> ir_to_identifier(ir)
<> "' - failed to decode 'evaluation' field as string",
)
}),
)
templatizer.parse_and_resolve_query_template(
evaluation_string,
ir.values,
from: identifier,
)
|> result.map(fn(resolved_evaluation) {
option.Some(helpers.ValueTuple(
"evaluation",
PrimitiveType(StringType),
value.StringValue(resolved_evaluation),
))
})
}
})
// Update the IR with the resolved indicators and evaluation.
let new_values =
ir.values
|> list.map(fn(vt) {
case vt.label {
"indicators" -> new_indicators_value_tuple
"evaluation" ->
case resolved_evaluation_tuple {
option.Some(new_evaluation) -> new_evaluation
option.None -> vt
}
_ -> vt
}
})
// Also update the structured artifact_data with resolved values.
let resolved_indicators_dict = resolved_indicators |> dict.from_list
let resolved_eval = case resolved_evaluation_tuple {
option.Some(vt) -> value.extract_string(vt.value) |> option.from_result
option.None ->
ir.get_slo_fields(ir.artifact_data)
|> option.map(fn(slo) { slo.evaluation })
|> option.unwrap(option.None)
}
let new_artifact_data =
ir.update_slo_fields(ir.artifact_data, fn(slo) {
SloFields(
..slo,
indicators: resolved_indicators_dict,
evaluation: resolved_eval,
)
})
Ok(
IntermediateRepresentation(
..ir,
values: new_values,
artifact_data: new_artifact_data,
),
)
}
/// Default evaluation expression used when no explicit evaluation is provided.
const default_evaluation = "numerator / denominator"
/// Generate Terraform HCL from a list of Datadog IntermediateRepresentations.
/// Includes provider configuration and variables.
/// Note: Datadog does not use generator_utils.generate_terraform because it
/// returns warnings alongside the HCL string.
pub fn generate_terraform(
irs: List(IntermediateRepresentation(Resolved)),
) -> Result(#(String, List(String)), CompilationError) {
use #(resources, warnings) <- result.try(generate_resources(irs))
Ok(#(
generator_utils.render_terraform_config(
resources: resources,
settings: terraform_settings(),
providers: [provider()],
variables: variables(),
),
warnings,
))
}
/// Generate only the Terraform resources for Datadog IRs (no config/provider).
@internal
pub fn generate_resources(
irs: List(IntermediateRepresentation(Resolved)),
) -> Result(#(List(terraform.Resource), List(String)), CompilationError) {
irs
|> list.try_fold(#([], []), fn(acc, ir) {
let #(resources, warning_lists) = acc
use #(resource, ir_warnings) <- result.try(ir_to_terraform_resource(ir))
Ok(#([resource, ..resources], [ir_warnings, ..warning_lists]))
})
|> result.map(fn(pair) {
#(list.reverse(pair.0), list.flatten(list.reverse(pair.1)))
})
}
/// Terraform settings block with required Datadog provider.
@internal
pub fn terraform_settings() -> terraform.TerraformSettings {
generator_utils.build_terraform_settings(
provider_name: constants.provider_datadog,
source: "DataDog/datadog",
version: "~> 3.0",
)
}
/// Datadog provider configuration using variables for credentials.
@internal
pub fn provider() -> terraform.Provider {
generator_utils.build_provider(name: constants.provider_datadog, attributes: [
#("api_key", hcl.ref("var.datadog_api_key")),
#("app_key", hcl.ref("var.datadog_app_key")),
])
}
/// Variables for Datadog API credentials.
@internal
pub fn variables() -> List(terraform.Variable) {
[
terraform.Variable(
name: "datadog_api_key",
type_constraint: option.Some(hcl.Identifier("string")),
default: option.None,
description: option.Some("Datadog API key"),
sensitive: option.Some(True),
nullable: option.None,
validation: [],
),
terraform.Variable(
name: "datadog_app_key",
type_constraint: option.Some(hcl.Identifier("string")),
default: option.None,
description: option.Some("Datadog Application key"),
sensitive: option.Some(True),
nullable: option.None,
validation: [],
),
]
}
/// Convert a single IntermediateRepresentation to a Terraform Resource.
/// Uses CQL to parse the value expression and generate HCL blocks.
@internal
pub fn ir_to_terraform_resource(
ir: IntermediateRepresentation(Resolved),
) -> Result(#(terraform.Resource, List(String)), CompilationError) {
let resource_name = common.sanitize_terraform_identifier(ir.unique_identifier)
// Extract structured SLO fields from IR.
use slo <- result.try(generator_utils.require_slo_fields(
ir,
vendor: constants.vendor_datadog,
))
let threshold = slo.threshold
let window_in_days = slo.window_in_days
let indicators = slo.indicators
let evaluation_expr = slo.evaluation |> option.unwrap(default_evaluation)
let runbook = slo.runbook
// Parse the evaluation expression using CQL and get HCL blocks.
use cql_generator.ResolvedSloHcl(slo_type, slo_blocks) <- result.try(
cql_generator.resolve_slo_to_hcl(evaluation_expr, indicators)
|> result.map_error(fn(err) {
errors.generator_slo_query_resolution_error(
msg: "expectation '"
<> ir_to_identifier(ir)
<> "' - failed to resolve SLO query: "
<> err,
)
}),
)
// Build dependency relation tags if artifact refs include DependencyRelations.
let dependency_tags = case ir.get_dependency_fields(ir.artifact_data) {
option.Some(dep) -> build_dependency_tags(dep.relations)
option.None -> []
}
// Build user-provided tags as key-value pairs.
let user_tag_pairs = slo.tags
// Build system tags from IR metadata.
let system_tag_pairs =
helpers.build_system_tag_pairs(
org_name: ir.metadata.org_name,
team_name: ir.metadata.team_name,
service_name: ir.metadata.service_name,
blueprint_name: ir.metadata.blueprint_name,
friendly_label: ir.metadata.friendly_label,
artifact_refs: ir.artifact_refs,
misc: ir.metadata.misc,
)
|> list.append(dependency_tags)
// Detect overshadowing: user tags whose key matches a system tag key.
let system_tag_keys =
system_tag_pairs |> list.map(fn(pair) { pair.0 }) |> set.from_list
let user_tag_keys =
user_tag_pairs |> list.map(fn(pair) { pair.0 }) |> set.from_list
let overlapping_keys = set.intersection(system_tag_keys, user_tag_keys)
// Collect warnings about overshadowing and filter out overshadowed system tags.
let #(final_system_tag_pairs, warnings) = case
set.size(overlapping_keys) > 0
{
True -> {
let warn_msgs =
overlapping_keys
|> set.to_list
|> list.sort(string.compare)
|> list.map(fn(key) {
ir_to_identifier(ir)
<> " - user tag '"
<> key
<> "' overshadows system tag"
})
let filtered =
system_tag_pairs
|> list.filter(fn(pair) { !set.contains(overlapping_keys, pair.0) })
#(filtered, warn_msgs)
}
False -> #(system_tag_pairs, [])
}
let tags =
list.append(final_system_tag_pairs, user_tag_pairs)
|> list.map(fn(pair) { hcl.StringLiteral(pair.0 <> ":" <> pair.1) })
|> hcl.ListExpr
let identifier = ir_to_identifier(ir)
use window_in_days_string <- result.try(
window_to_timeframe(window_in_days)
|> result.map_error(fn(err) { errors.prefix_error(err, identifier) }),
)
// Build the thresholds block (common to both types).
let thresholds_block =
hcl.simple_block("thresholds", [
#("timeframe", hcl.StringLiteral(window_in_days_string)),
#("target", hcl.FloatLiteral(threshold)),
])
let type_str = case slo_type {
cql_generator.TimeSliceSlo -> "time_slice"
cql_generator.MetricSlo -> "metric"
}
let base_attributes = [
#("name", hcl.StringLiteral(ir.metadata.friendly_label.value)),
#("type", hcl.StringLiteral(type_str)),
#("tags", tags),
]
let attributes = case runbook {
option.Some(url) -> [
#("description", hcl.StringLiteral("[Runbook](" <> url <> ")")),
..base_attributes
]
option.None -> base_attributes
}
Ok(#(
terraform.Resource(
type_: "datadog_service_level_objective",
name: resource_name,
attributes: dict.from_list(attributes),
blocks: list.append(slo_blocks, [thresholds_block]),
meta: hcl.empty_meta(),
lifecycle: option.None,
),
warnings,
))
}
/// Build dependency relation tag pairs from the relations dict.
/// Generates pairs like #("soft_dependency", "target1,target2").
fn build_dependency_tags(
relations: dict.Dict(artifacts.DependencyRelationType, List(String)),
) -> List(#(String, String)) {
relations
|> dict.to_list
|> list.sort(fn(a, b) {
string.compare(
artifacts.relation_type_to_string(a.0),
artifacts.relation_type_to_string(b.0),
)
})
|> list.map(fn(pair) {
let #(relation_type, targets) = pair
let sorted_targets = targets |> list.sort(string.compare)
#(
artifacts.relation_type_to_string(relation_type) <> "_dependency",
string.join(sorted_targets, ","),
)
})
}
/// Convert window_in_days to Datadog timeframe string.
/// Range (1-90) is guaranteed by the standard library; Datadog further restricts to {7, 30, 90}.
@internal
pub fn window_to_timeframe(days: Int) -> Result(String, CompilationError) {
let days_string = int.to_string(days)
case days {
7 | 30 | 90 -> Ok(days_string <> "d")
_ ->
Error(generator_utils.resolution_error(
vendor: constants.vendor_datadog,
msg: "Illegal window_in_days value: "
<> days_string
<> ". Accepted values are 7, 30, or 90.",
))
}
}