Packages
caffeine_lang
4.4.3
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/newrelic.gleam
import caffeine_lang/codegen/generator_utils
import caffeine_lang/constants
import caffeine_lang/errors.{type CompilationError}
import caffeine_lang/linker/ir.{
type IntermediateRepresentation, ir_to_identifier,
}
import gleam/dict
import gleam/int
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 New Relic IntermediateRepresentations.
pub fn generate_terraform(
irs: List(IntermediateRepresentation),
) -> 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 New Relic IRs (no config/provider).
@internal
pub fn generate_resources(
irs: List(IntermediateRepresentation),
) -> Result(#(List(terraform.Resource), List(String)), CompilationError) {
generator_utils.generate_resources_simple(
irs,
mapper: ir_to_terraform_resource,
)
}
/// Terraform settings block with required New Relic provider.
@internal
pub fn terraform_settings() -> terraform.TerraformSettings {
generator_utils.build_terraform_settings(
provider_name: constants.provider_newrelic,
source: "newrelic/newrelic",
version: "~> 3.0",
)
}
/// New Relic provider configuration using variables for credentials.
@internal
pub fn provider() -> terraform.Provider {
generator_utils.build_provider(name: constants.provider_newrelic, attributes: [
#("account_id", hcl.ref("var.newrelic_account_id")),
#("api_key", hcl.ref("var.newrelic_api_key")),
#("region", hcl.ref("var.newrelic_region")),
])
}
/// Variables for New Relic account ID, API key, and region.
@internal
pub fn variables() -> List(terraform.Variable) {
[
terraform.Variable(
name: "newrelic_account_id",
type_constraint: option.Some(hcl.Identifier("number")),
default: option.None,
description: option.Some("New Relic account ID"),
sensitive: option.None,
nullable: option.None,
validation: [],
),
terraform.Variable(
name: "newrelic_api_key",
type_constraint: option.Some(hcl.Identifier("string")),
default: option.None,
description: option.Some("New Relic API key"),
sensitive: option.Some(True),
nullable: option.None,
validation: [],
),
terraform.Variable(
name: "newrelic_region",
type_constraint: option.Some(hcl.Identifier("string")),
default: option.Some(hcl.StringLiteral("US")),
description: option.Some("New Relic region"),
sensitive: option.None,
nullable: option.None,
validation: [],
),
terraform.Variable(
name: "newrelic_entity_guid",
type_constraint: option.Some(hcl.Identifier("string")),
default: option.None,
description: option.Some("New Relic entity GUID"),
sensitive: option.None,
nullable: option.None,
validation: [],
),
]
}
/// Convert a single IntermediateRepresentation to a New Relic Terraform Resource.
/// Produces a single `newrelic_service_level` resource.
@internal
pub fn ir_to_terraform_resource(
ir: IntermediateRepresentation,
) -> Result(terraform.Resource, CompilationError) {
let identifier = ir_to_identifier(ir)
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_newrelic,
))
// Extract the evaluation expression, then resolve it through the CQL pipeline.
use evaluation_expr <- result.try(generator_utils.require_evaluation(
slo,
ir,
vendor: constants.vendor_newrelic,
))
use _ <- result.try(generator_utils.resolve_cql_expression(
evaluation_expr,
slo.indicators,
ir,
vendor: constants.vendor_newrelic,
))
use rolling_count <- result.try(
window_to_rolling_count(slo.window_in_days)
|> result.map_error(fn(err) { errors.prefix_error(err, identifier) }),
)
// Build the events block from indicators.
use events_block <- result.try(build_events_block(
slo.indicators,
evaluation_expr,
identifier,
))
let description = generator_utils.build_description(ir, with: slo)
let resource =
terraform.Resource(
type_: "newrelic_service_level",
name: resource_name,
attributes: dict.from_list([
#("guid", hcl.ref("var.newrelic_entity_guid")),
#("name", hcl.StringLiteral(ir.metadata.friendly_label)),
#("description", hcl.StringLiteral(description)),
]),
blocks: [
events_block,
hcl.Block(
type_: "objective",
labels: [],
attributes: dict.from_list([
#("target", hcl.FloatLiteral(slo.threshold)),
]),
blocks: [
hcl.Block(
type_: "time_window",
labels: [],
attributes: dict.new(),
blocks: [
hcl.simple_block("rolling", [
#("count", hcl.IntLiteral(rolling_count)),
#("unit", hcl.StringLiteral("DAY")),
]),
],
),
],
),
],
meta: hcl.empty_meta(),
lifecycle: option.None,
)
Ok(resource)
}
/// Build the events block from indicators and evaluation expression.
/// Determines which indicators map to valid_events and good_events based
/// on the evaluation expression structure (numerator = good, denominator = valid).
fn build_events_block(
indicators: dict.Dict(String, String),
evaluation_expr: String,
identifier: String,
) -> Result(hcl.Block, CompilationError) {
// The evaluation expression is expected to be "good / valid" format.
// The numerator indicator maps to good_events, denominator to valid_events.
let indicator_names = dict.keys(indicators)
use #(good_name, valid_name) <- result.try(extract_good_valid_names(
evaluation_expr,
indicator_names,
identifier,
))
use good_indicator <- result.try(
dict.get(indicators, good_name)
|> result.replace_error(generator_utils.resolution_error(
vendor: constants.vendor_newrelic,
msg: "expectation '"
<> identifier
<> "' - indicator '"
<> good_name
<> "' not found",
)),
)
use valid_indicator <- result.try(
dict.get(indicators, valid_name)
|> result.replace_error(generator_utils.resolution_error(
vendor: constants.vendor_newrelic,
msg: "expectation '"
<> identifier
<> "' - indicator '"
<> valid_name
<> "' not found",
)),
)
let #(good_from, good_where) = parse_nrql_indicator(good_indicator)
let #(valid_from, valid_where) = parse_nrql_indicator(valid_indicator)
let good_events_block =
build_nrql_event_block("good_events", good_from, good_where)
let valid_events_block =
build_nrql_event_block("valid_events", valid_from, valid_where)
Ok(
hcl.Block(
type_: "events",
labels: [],
attributes: dict.from_list([
#("account_id", hcl.ref("var.newrelic_account_id")),
]),
blocks: [valid_events_block, good_events_block],
),
)
}
/// Extract good and valid indicator names from the evaluation expression.
/// Expects a "numerator / denominator" pattern where numerator = good, denominator = valid.
fn extract_good_valid_names(
evaluation_expr: String,
indicator_names: List(String),
identifier: String,
) -> Result(#(String, String), CompilationError) {
case string.split(evaluation_expr, " / ") {
[good, valid] -> {
let good_trimmed = string.trim(good)
let valid_trimmed = string.trim(valid)
case
list.contains(indicator_names, good_trimmed)
&& list.contains(indicator_names, valid_trimmed)
{
True -> Ok(#(good_trimmed, valid_trimmed))
False ->
Error(generator_utils.resolution_error(
vendor: constants.vendor_newrelic,
msg: "expectation '"
<> identifier
<> "' - evaluation references indicators not found in indicator map",
))
}
}
_ ->
Error(generator_utils.resolution_error(
vendor: constants.vendor_newrelic,
msg: "expectation '"
<> identifier
<> "' - evaluation must be in 'good / valid' format for New Relic",
))
}
}
/// Parse a NRQL indicator string into (from, optional where) components.
/// Format: "EventType" or "EventType WHERE condition".
@internal
pub fn parse_nrql_indicator(
indicator: String,
) -> #(String, option.Option(String)) {
case string.split(indicator, " WHERE ") {
[from] -> #(from, option.None)
[from, ..rest] -> #(from, option.Some(string.join(rest, " WHERE ")))
_ -> #(indicator, option.None)
}
}
/// Build an NRQL event sub-block (good_events or valid_events).
fn build_nrql_event_block(
block_type: String,
from: String,
where_clause: option.Option(String),
) -> hcl.Block {
let attrs = case where_clause {
option.Some(where) -> [
#("from", hcl.StringLiteral(from)),
#("where", hcl.StringLiteral(where)),
]
option.None -> [#("from", hcl.StringLiteral(from))]
}
hcl.Block(
type_: block_type,
labels: [],
attributes: dict.from_list(attrs),
blocks: [],
)
}
/// Convert window_in_days to New Relic rolling count.
/// New Relic only accepts 1, 7, or 28 day windows.
@internal
pub fn window_to_rolling_count(days: Int) -> Result(Int, CompilationError) {
case days {
1 | 7 | 28 -> Ok(days)
_ ->
Error(generator_utils.resolution_error(
vendor: constants.vendor_newrelic,
msg: "Illegal window_in_days value: "
<> int.to_string(days)
<> ". New Relic accepts only 1, 7, or 28.",
))
}
}