Packages
caffeine_lang
5.0.11
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/honeycomb.gleam
import caffeine_lang/codegen/generator_utils
import caffeine_lang/constants
import caffeine_lang/errors.{type CompilationError}
import caffeine_lang/helpers
import caffeine_lang/linker/ir.{type IntermediateRepresentation, type Resolved}
import gleam/dict
import gleam/list
import gleam/option
import gleam/result
import gleam/string
import terra_madre/common
import terra_madre/hcl
import terra_madre/terraform
/// Generate Terraform HCL from a list of Honeycomb IntermediateRepresentations.
pub fn generate_terraform(
irs: List(IntermediateRepresentation(Resolved)),
) -> Result(String, CompilationError) {
generator_utils.generate_terraform(
irs,
settings: terraform_settings(),
provider: provider(),
variables: variables(),
generate_resources: generate_resources,
)
}
/// Generate only the Terraform resources for Honeycomb IRs (no config/provider).
@internal
pub fn generate_resources(
irs: List(IntermediateRepresentation(Resolved)),
) -> Result(#(List(terraform.Resource), List(String)), CompilationError) {
generator_utils.generate_resources_multi(
irs,
mapper: ir_to_terraform_resources,
)
}
/// Terraform settings block with required Honeycomb provider.
@internal
pub fn terraform_settings() -> terraform.TerraformSettings {
generator_utils.build_terraform_settings(
provider_name: constants.provider_honeycombio,
source: "honeycombio/honeycombio",
version: "~> 0.31",
)
}
/// Honeycomb provider configuration using variables for credentials.
@internal
pub fn provider() -> terraform.Provider {
generator_utils.build_provider(
name: constants.provider_honeycombio,
attributes: [#("api_key", hcl.ref("var.honeycomb_api_key"))],
)
}
/// Variables for Honeycomb API credentials and dataset.
@internal
pub fn variables() -> List(terraform.Variable) {
[
terraform.Variable(
name: "honeycomb_api_key",
type_constraint: option.Some(hcl.Identifier("string")),
default: option.None,
description: option.Some("Honeycomb API key"),
sensitive: option.Some(True),
nullable: option.None,
validation: [],
),
terraform.Variable(
name: "honeycomb_dataset",
type_constraint: option.Some(hcl.Identifier("string")),
default: option.None,
description: option.Some("Honeycomb dataset slug"),
sensitive: option.None,
nullable: option.None,
validation: [],
),
]
}
/// Convert a single IntermediateRepresentation to Honeycomb Terraform Resources.
/// Produces a derived_column for the SLI and an SLO that references it.
@internal
pub fn ir_to_terraform_resources(
ir: IntermediateRepresentation(Resolved),
) -> Result(List(terraform.Resource), CompilationError) {
let resource_name = common.sanitize_terraform_identifier(ir.unique_identifier)
let slo = ir.slo
let threshold = slo.threshold
let window_in_days = slo.window_in_days
let indicators = slo.indicators
// Extract the evaluation expression, then resolve it through the CQL pipeline
// by substituting indicator names into the evaluation formula.
use evaluation_expr <- result.try(generator_utils.require_evaluation(
slo,
ir,
vendor: constants.vendor_honeycomb,
))
use sli_expression <- result.try(generator_utils.resolve_cql_expression(
evaluation_expr,
indicators,
ir,
vendor: constants.vendor_honeycomb,
))
let time_period = window_to_time_period(window_in_days)
let derived_column_alias = resource_name <> "_sli"
// Resource 1: honeycombio_derived_column for the SLI.
let derived_column =
terraform.Resource(
type_: "honeycombio_derived_column",
name: derived_column_alias,
attributes: dict.from_list([
#("alias", hcl.StringLiteral(derived_column_alias)),
#("expression", hcl.StringLiteral(sli_expression)),
#("dataset", hcl.ref("var.honeycomb_dataset")),
]),
blocks: [],
meta: hcl.empty_meta(),
lifecycle: option.None,
)
// Resource 2: honeycombio_slo.
let slo_description = generator_utils.build_description(ir, with: slo)
let slo_attributes = [
#("name", hcl.StringLiteral(ir.metadata.friendly_label.value)),
#("description", hcl.StringLiteral(slo_description)),
#("dataset", hcl.ref("var.honeycomb_dataset")),
#(
"sli",
hcl.ref("honeycombio_derived_column." <> derived_column_alias <> ".alias"),
),
#("target_percentage", hcl.FloatLiteral(threshold)),
#("time_period", hcl.IntLiteral(time_period)),
#("tags", build_tags(ir)),
]
let slo =
terraform.Resource(
type_: "honeycombio_slo",
name: resource_name,
attributes: dict.from_list(slo_attributes),
blocks: [],
meta: hcl.empty_meta(),
lifecycle: option.None,
)
Ok([derived_column, slo])
}
/// Build tags as a map expression for Honeycomb.
/// Honeycomb tags: keys ^[a-z]{1,32}$, values ^[a-z][a-z0-9/-]{1,128}$.
/// Max 10 tags per resource.
fn build_tags(ir: IntermediateRepresentation(Resolved)) -> hcl.Expr {
// Build system tags from shared helper. For Honeycomb, misc tags with multiple
// values are joined with forward slashes since commas are not valid in tag values.
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,
measurement_name: ir.metadata.measurement_name,
friendly_label: ir.metadata.friendly_label,
misc: ir.metadata.misc,
)
|> collapse_multi_value_tags
// Build user-provided tags from structured SLO data.
let user_tag_pairs = ir.slo.tags
let all_tags =
list.append(system_tag_pairs, user_tag_pairs)
|> list.map(fn(pair) {
#(
hcl.IdentKey(sanitize_honeycomb_tag_key(pair.0)),
hcl.StringLiteral(sanitize_honeycomb_tag_value(pair.1)),
)
})
|> list.take(max_tags_per_resource)
hcl.MapExpr(all_tags)
}
const max_tags_per_resource = 10
/// Sanitize a tag key for Honeycomb: must match ^[a-z]{1,32}$.
/// Removes underscores, hyphens, digits, and any non-letter characters.
@internal
pub fn sanitize_honeycomb_tag_key(key: String) -> String {
key
|> string.lowercase
|> string.to_graphemes
|> list.filter(is_lowercase_letter)
|> string.concat
|> string.slice(0, 32)
}
/// Sanitize a tag value for Honeycomb: must match ^[a-z][a-z0-9/-]{1,128}$.
/// Spaces, underscores, and commas become hyphens; uppercase becomes lowercase;
/// values starting with a non-letter get a "v" prefix.
@internal
pub fn sanitize_honeycomb_tag_value(value: String) -> String {
let sanitized =
value
|> string.lowercase
|> string.to_graphemes
|> list.map(fn(char) {
case char {
" " | "_" | "," -> "-"
_ ->
case is_valid_tag_value_char(char) {
True -> char
False -> ""
}
}
})
|> string.concat
// Ensure value starts with a lowercase letter.
let sanitized = case string.first(sanitized) {
Ok(c) ->
case is_lowercase_letter(c) {
True -> sanitized
False -> "v" <> sanitized
}
_ -> sanitized
}
sanitized
|> string.slice(0, 129)
}
fn is_lowercase_letter(char: String) -> Bool {
case string.to_utf_codepoints(char) {
[cp] -> {
let code = string.utf_codepoint_to_int(cp)
code >= 97 && code <= 122
}
_ -> False
}
}
fn is_valid_tag_value_char(char: String) -> Bool {
case char {
"-" | "/" -> True
_ -> is_lowercase_letter(char) || is_digit(char)
}
}
fn is_digit(char: String) -> Bool {
case string.to_utf_codepoints(char) {
[cp] -> {
let code = string.utf_codepoint_to_int(cp)
code >= 48 && code <= 57
}
_ -> False
}
}
/// Collapse tag pairs that share the same key by joining values with forward slashes.
/// The shared helper produces one pair per misc value, but Honeycomb needs a
/// single key-value entry per tag key. Preserves insertion order of first occurrence.
fn collapse_multi_value_tags(
pairs: List(#(String, String)),
) -> List(#(String, String)) {
let #(order, merged) =
pairs
|> list.fold(#([], dict.new()), fn(acc, pair) {
let #(keys, seen) = acc
let #(key, value) = pair
case dict.get(seen, key) {
Ok(existing) -> #(
keys,
dict.insert(seen, key, existing <> "/" <> value),
)
Error(_) -> #([key, ..keys], dict.insert(seen, key, value))
}
})
order
|> list.reverse
|> list.filter_map(fn(key) {
dict.get(merged, key)
|> result.map(fn(value) { #(key, value) })
})
}
/// Convert window_in_days to Honeycomb time_period (in days).
/// Range (1-90) is guaranteed by the standard library type constraint.
@internal
pub fn window_to_time_period(days: Int) -> Int {
days
}