Packages
caffeine_lang
4.4.4
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/analysis/templatizer.gleam
/// Datadog Query Template Resolver
///
/// This module defines template types for replacing placeholders in Datadog metric queries.
///
/// Two template formats are supported:
///
/// 1. Simple raw value substitution: `$$INPUT_NAME$$`
/// - Just substitutes the raw value with no formatting
/// - Example: `time_slice(query < $$threshold$$ per 10s)` with threshold=2500000
/// becomes `time_slice(query < 2500000 per 10s)`
///
/// 2. Datadog filter format: `$$INPUT_NAME->DATADOG_ATTR:TEMPLATE_TYPE$$`
/// - INPUT_NAME: The name of the input attribute from the expectation (provides the value)
/// - DATADOG_ATTR: The Datadog tag/attribute name to use in the query
/// - TEMPLATE_TYPE: How the value should be formatted in the query (optional, defaults to tag format)
/// - Example: `$$environment->env$$` with environment="production"
/// becomes `env:production`
///
/// Datadog Query Syntax Reference:
/// - Tag filters: `{tag:value}` or `{tag:value, tag2:value2}`
/// - Boolean operators: AND, OR, NOT (or symbolic: !)
/// - Wildcards: `tag:prefix*` or `tag:*suffix`
/// - IN operator: `tag IN (value1, value2, value3)`
/// - NOT IN operator: `tag NOT IN (value1, value2)`
///
/// Sources:
/// - https://docs.datadoghq.com/metrics/advanced-filtering/
/// - https://www.datadoghq.com/blog/boolean-filtered-metric-queries/
/// - https://www.datadoghq.com/blog/wildcard-filter-queries/
import caffeine_lang/errors.{type CompilationError}
import caffeine_lang/helpers.{type ValueTuple}
import caffeine_lang/types
import gleam/list
import gleam/result
import gleam/string
/// A parsed template variable containing all the information needed for substitution.
pub type TemplateVariable {
TemplateVariable(
// The name of the input attribute from the expectation (provides the value).
input_name: String,
// The Datadog tag/attribute name to use in the query.
datadog_attr: String,
// How the value should be formatted.
template_type: DatadogTemplateType,
)
}
/// Template types for Datadog query value replacement.
/// Each type defines how an input value should be formatted in the final query.
///
/// Most formatting is auto-detected from the value:
/// - String -> `attr:value`
/// - List -> `attr IN (value1, value2, ...)`
/// - Value containing `*` -> wildcard preserved as-is
///
/// The only explicit type needed is `Not` for negation.
pub type DatadogTemplateType {
// Raw type for simple value substitution without any formatting.
// Used when template is just `$$name$$` without `->`.
// - String -> just the value itself
// - List -> comma-separated values
Raw
// Default type that auto-detects based on value:
// - String -> `attr:value` (wildcards in value are preserved)
// - List -> `attr IN (value1, value2, ...)`
Default
// Negated filter (auto-detects based on value):
// - String -> `!attr:value` (wildcards in value are preserved)
// - List -> `attr NOT IN (value1, value2, ...)`
Not
}
/// High-level parsing and resolution of a templatized query string.
@internal
pub fn parse_and_resolve_query_template(
query: String,
value_tuples: List(ValueTuple),
from identifier: String,
) -> Result(String, CompilationError) {
use resolved <- result.try(
do_parse_and_resolve_query_template(query, value_tuples)
|> result.map_error(fn(err) { errors.prefix_error(err, identifier) }),
)
Ok(cleanup_empty_template_artifacts(resolved))
}
/// Internal recursive implementation of template resolution.
fn do_parse_and_resolve_query_template(
query: String,
value_tuples: List(ValueTuple),
) -> Result(String, CompilationError) {
case string.split_once(query, "$$") {
// No more `$$`.
Error(_) -> Ok(query)
Ok(#(before, rest)) -> {
case string.split_once(rest, "$$") {
Error(_) ->
Error(errors.semantic_analysis_template_parse_error(
msg: "Unexpected incomplete `$$` for substring: " <> query,
))
Ok(#(inside, rest)) -> {
use rest_of_items <- result.try(do_parse_and_resolve_query_template(
rest,
value_tuples,
))
use template <- result.try(parse_template_variable(inside))
use value_tuple <- result.try(
value_tuples
|> list.find(fn(vt) { vt.label == template.input_name })
|> result.replace_error(
errors.semantic_analysis_template_parse_error(
msg: "Missing input for template: " <> template.input_name,
),
),
)
use resolved_template <- result.try(resolve_template(
template,
value_tuple,
))
Ok(before <> resolved_template <> rest_of_items)
}
}
}
}
}
/// Cleans up artifacts from empty optional template resolutions.
/// When optional fields resolve to empty strings, they can leave behind
/// hanging commas in the query. This function removes those artifacts.
///
/// Examples:
/// - "{env:prod, }" -> "{env:prod}"
/// - "{, env:prod}" -> "{env:prod}"
/// - "{env:prod, , region:us}" -> "{env:prod, region:us}"
/// - "(, value1)" -> "(value1)"
/// - "(value1, )" -> "(value1)"
@internal
pub fn cleanup_empty_template_artifacts(query: String) -> String {
let cleaned = do_cleanup(query)
case cleaned == query {
True -> cleaned
False -> cleanup_empty_template_artifacts(cleaned)
}
}
fn do_cleanup(query: String) -> String {
query
// Handle consecutive empty optionals first (before bracket cleanup)
|> string.replace(", ,", ",")
|> string.replace(",,", ",")
|> string.replace(" AND AND ", " AND ")
// Handle ", }" and ",}" - empty optional at end of filter
|> string.replace(", }", "}")
|> string.replace(",}", "}")
// Handle "{, " and "{," - empty optional at start of filter
|> string.replace("{, ", "{")
|> string.replace("{,", "{")
// Handle ", )" and ",)" - empty optional at end of IN clause
|> string.replace(", )", ")")
|> string.replace(",)", ")")
// Handle "(, " and "(," - empty optional at start of IN clause
|> string.replace("(, ", "(")
|> string.replace("(,", "(")
// Handle " AND " with empty - e.g., " AND }" or "{ AND "
|> string.replace(" AND }", "}")
|> string.replace("{ AND ", "{")
// Handle remaining whitespace-only content in brackets
|> string.replace("{ }", "{}")
|> string.replace("( )", "()")
}
/// Parses a template variable string. Supports two formats:
///
/// 1. Simple raw value: "INPUT_NAME" (no ->)
/// - Returns Raw type for direct value substitution
/// - Example: "threshold" -> TemplateVariable("threshold", "", Raw)
///
/// 2. Datadog format: "INPUT_NAME->DATADOG_ATTR:TEMPLATE_TYPE"
/// - Returns Default or Not type for Datadog filter formatting
/// - Example: "environment->env" -> TemplateVariable("environment", "env", Default)
/// - Example: "environment->env:not" -> TemplateVariable("environment", "env", Not)
@internal
pub fn parse_template_variable(
variable: String,
) -> Result(TemplateVariable, CompilationError) {
case string.split_once(variable, "->") {
// No "->" means simple raw value substitution.
Error(_) -> {
let trimmed = string.trim(variable)
case trimmed {
"" ->
Error(errors.semantic_analysis_template_parse_error(
msg: "Empty template variable name: " <> variable,
))
_ ->
Ok(TemplateVariable(
input_name: trimmed,
datadog_attr: "",
template_type: Raw,
))
}
}
// Has "->" means Datadog format.
Ok(#(input_name, rest)) -> {
let trimmed_input = string.trim(input_name)
case trimmed_input, rest {
"", _ ->
Error(errors.semantic_analysis_template_parse_error(
msg: "Empty input name in template: " <> variable,
))
_, "" ->
Error(errors.semantic_analysis_template_parse_error(
msg: "Empty label name in template: " <> variable,
))
_, _ -> parse_datadog_template_variable(trimmed_input, rest)
}
}
}
}
/// Parses a template type string into a DatadogTemplateType.
///
/// Supported template type strings:
/// - "not" -> Not
@internal
pub fn parse_template_type(
type_string: String,
) -> Result(DatadogTemplateType, CompilationError) {
case type_string {
"not" -> Ok(Not)
_ ->
Error(errors.semantic_analysis_template_parse_error(
msg: "Unknown template type: " <> type_string,
))
}
}
/// Given a parsed template and a parsed value tuple, resolve the templatized string.
/// ASSUMPTION: we already checked the value type is correct in the parser phase.
@internal
pub fn resolve_template(
template: TemplateVariable,
value_tuple: ValueTuple,
) -> Result(String, CompilationError) {
use _ <- result.try(case template.input_name == value_tuple.label {
True -> Ok(Nil)
_ ->
Error(errors.semantic_analysis_template_resolution_error(
msg: "Mismatch between template input name ("
<> template.input_name
<> ") and input value label ("
<> value_tuple.label
<> ").",
))
})
case
types.resolve_to_string(
value_tuple.typ,
value_tuple.value,
resolve_string_value(template, _),
resolve_list_value(template, _),
)
{
Ok(resolved) -> Ok(resolved)
Error(msg) ->
Error(errors.semantic_analysis_template_resolution_error(msg:))
}
}
/// Formats a string value according to the template variable.
/// Returns the formatted string ready for insertion into a Datadog query.
/// Wildcards in the value are preserved as-is.
/// ASSUMPTION: we already checked the value type is correct and the label matches
/// the Datadog template name. Thus instead of passing in a ValueTuple
/// we can just pass in the raw string value.
@internal
pub fn resolve_string_value(template: TemplateVariable, value: String) -> String {
let attr = template.datadog_attr
case template.template_type {
Raw -> value
Default -> attr <> ":" <> value
Not -> "!" <> attr <> ":" <> value
}
}
/// Formats a list of string values according to the template variable.
/// ASSUMPTION: we already checked the value type is correct and the label matches
/// the Datadog template name. Thus instead of passing in a ValueTuple
/// we can just pass in the raw list value of strings.
@internal
pub fn resolve_list_value(
template: TemplateVariable,
values: List(String),
) -> String {
let attr = template.datadog_attr
case template.template_type, values {
_, [] -> ""
Raw, values -> values |> string.join(", ")
Default, values -> attr <> " IN (" <> values |> string.join(", ") <> ")"
Not, values -> attr <> " NOT IN (" <> values |> string.join(", ") <> ")"
}
}
/// Helper to parse Datadog format template variables (with ->).
fn parse_datadog_template_variable(
input_name: String,
rest: String,
) -> Result(TemplateVariable, CompilationError) {
case string.split_once(rest, ":") {
// No colon means default template type.
Error(_) ->
Ok(TemplateVariable(
input_name: string.trim(rest),
datadog_attr: input_name,
template_type: Default,
))
Ok(#(datadog_attr, type_string)) -> {
case parse_template_type(string.trim(type_string)) {
Error(e) -> Error(e)
Ok(template_type) ->
Ok(TemplateVariable(
input_name: string.trim(datadog_attr),
datadog_attr: input_name,
template_type: template_type,
))
}
}
}
}