From 38a13d6f53257032fcf11b4191d3658bf822b612 Mon Sep 17 00:00:00 2001 From: Arsen Shkrumelyak Date: Thu, 12 Nov 2026 19:59:53 +0000 Subject: [PATCH 1/8] fix(stack): forward every selected namespace from every compartment --- .github/workflows/test.yml | 1 + README.md | 4 +- main.tf | 53 +++++----- schema.yaml | 6 +- tests/connectors.tftest.hcl | 190 ++++++++++++++++++++++++++++++++++++ variables.tf | 5 +- 6 files changed, 225 insertions(+), 34 deletions(-) create mode 100644 tests/connectors.tftest.hcl diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6e23180..86c2ff9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,3 +25,4 @@ jobs: - run: terraform fmt -check - run: terraform init -backend=false - run: terraform validate + - run: terraform test diff --git a/README.md b/README.md index 1945835..1d39e8b 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,10 @@ The stack sends metrics and logs from the home region only. - A `betterstack` compartment for all the resources below. - A Function application for the forwarder. It runs in a VCN with a private subnet. A NAT gateway lets the Function reach Better Stack. -- One Connector Hub for logs, and one or more Connector Hubs for metrics. Each Connector Hub takes up to 5 compartments and 50 metric namespaces. A large tenancy can need a higher Connector Hub service limit. +- One Connector Hub for logs, and one or more Connector Hubs for metrics. Each Connector Hub takes up to 5 compartments and 50 metric namespaces, so it covers 2 compartments with all 25 namespaces selected, and 5 with 10 or fewer. If your region's Connector Hub limit is too low, Plan stops before anything changes. Deselect namespaces, list the compartments to forward metrics from, or request a limit increase. - A policy that lets the Connector Hubs read metrics and logs, and call the Function. -When the stack runs, it finds the compartments that have metrics in the selected namespaces. After you add compartments or use new services, apply the stack again. +Every selected namespace is forwarded from every compartment that exists when you apply, so services you start using later are forwarded without running Apply again. After you add compartments, apply the stack again. ## Uninstall diff --git a/main.tf b/main.tf index 9a41269..dec1fec 100644 --- a/main.tf +++ b/main.tf @@ -62,6 +62,15 @@ resource "oci_identity_policy" "connectors" { "Allow any-user to use fn-function in compartment id ${oci_identity_compartment.betterstack.id} ${local.connector_principal}", "Allow any-user to use fn-invocation in compartment id ${oci_identity_compartment.betterstack.id} ${local.connector_principal}", ]) + + # The policy comes before every connector, so this stops Plan before anything changes. + lifecycle { + # ponytail: compares against the whole regional limit and ignores Connector Hubs the tenancy already uses elsewhere; subtract the used count (oci_limits_resource_availability) if that bites. + precondition { + condition = local.connector_count <= local.connector_limit + error_message = "The stack needs ${local.connector_count} Connector Hubs, and your region allows ${local.connector_limit}. Deselect metric namespaces, list the compartments to forward metrics from, or request a Connector Hub limit increase." + } + } } # The Function has no public IP, so it reaches Better Stack through a NAT gateway. @@ -175,7 +184,7 @@ resource "oci_functions_function" "forwarder" { } # A monitoring source only covers its own compartment, not its children, so -# every compartment with metrics in a selected namespace becomes a source. +# every compartment becomes a source. Compartments added later need Apply again. data "oci_identity_compartments" "all" { compartment_id = var.tenancy_ocid compartment_id_in_subtree = true @@ -188,33 +197,23 @@ locals { [var.tenancy_ocid], [for compartment in data.oci_identity_compartments.all.compartments : compartment.id if compartment.name != "betterstack"], ) -} - -data "oci_monitoring_metrics" "namespaces" { - for_each = toset(local.metrics_compartment_ids) - compartment_id = each.value - group_by = ["namespace"] + # Every compartment gets every selected namespace, so a service first used + # later is forwarded without Apply. A connector takes at most 5 compartments + # and 50 namespaces across them. + metric_connectors = length(var.metrics_namespaces) == 0 ? [] : chunklist( + local.metrics_compartment_ids, + min(5, floor(50 / length(var.metrics_namespaces))), + ) + connector_count = length(local.metric_connectors) + (var.forward_audit_logs || length(local.log_group_ids) > 0 ? 1 : 0) + connector_limit = data.oci_limits_limit_values.connectors.limit_values[0].value } -locals { - metric_sources = [ - for id in local.metrics_compartment_ids : { - compartment_id = id - namespaces = sort(setintersection(var.metrics_namespaces, data.oci_monitoring_metrics.namespaces[id].metrics[*].namespace)) - } if length(setintersection(var.metrics_namespaces, data.oci_monitoring_metrics.namespaces[id].metrics[*].namespace)) > 0 - ] - - # A connector takes at most 5 compartments and 50 namespaces across them, so - # compartments are packed with others that select as many namespaces: one - # compartment with many namespaces doesn't shrink every other connector. - metric_source_sizes = sort(distinct([for source in local.metric_sources : format("%02d", length(source.namespaces))])) - metric_connectors = concat([], [ - for size in local.metric_source_sizes : chunklist( - [for source in local.metric_sources : source if length(source.namespaces) == tonumber(size)], - min(5, floor(50 / tonumber(size))), - ) - ]...) +data "oci_limits_limit_values" "connectors" { + compartment_id = var.tenancy_ocid + service_name = "service-connector-hub" + name = "service-connector-count" + scope_type = "REGION" } resource "oci_sch_service_connector" "metrics" { @@ -230,13 +229,13 @@ resource "oci_sch_service_connector" "metrics" { for_each = local.metric_connectors[count.index] content { - compartment_id = monitoring_sources.value.compartment_id + compartment_id = monitoring_sources.value namespace_details { kind = "selected" dynamic "namespaces" { - for_each = monitoring_sources.value.namespaces + for_each = var.metrics_namespaces content { namespace = namespaces.value diff --git a/schema.yaml b/schema.yaml index 82a359c..c597337 100644 --- a/schema.yaml +++ b/schema.yaml @@ -5,7 +5,7 @@ informationalText: Run this stack in the home region of your tenancy. It creates # The console only loads images from data: URIs and Oracle domains, so the logo is inline (logo.png). logoUrl: data:image/png;base64,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 schemaVersion: 1.1.0 -version: "20261110" +version: "20261112" locale: en variableGroups: @@ -43,14 +43,14 @@ variables: metrics_compartment_ids: title: Compartments - description: Compartment OCIDs to forward metrics from, separated by commas or new lines. Leave empty for every compartment. + description: Compartment OCIDs to forward metrics from, separated by commas or new lines. Leave empty for every compartment that exists when you apply. Apply again after you add compartments. type: text multiline: true required: false metrics_namespaces: title: Metric namespaces - description: Clear the list to forward logs only. + description: Forwarded from every compartment, including services you start using later. Each Connector Hub covers 2 compartments with all 25 namespaces, and 5 with 10 or fewer. Clear the list to forward logs only. type: enum additionalProps: allowMultiple: true diff --git a/tests/connectors.tftest.hcl b/tests/connectors.tftest.hcl new file mode 100644 index 0000000..e685b35 --- /dev/null +++ b/tests/connectors.tftest.hcl @@ -0,0 +1,190 @@ +mock_provider "oci" { + override_data { + target = data.oci_identity_region_subscriptions.home + values = { region_subscriptions = [{ region_name = "uk-london-1" }] } + } + + override_data { + target = data.oci_functions_functions_runtimes.python + values = { functions_runtime_collection = [{ items = [{ id = "runtime" }] }] } + } + + override_data { + target = data.oci_functions_functions_runtime_versions.python + values = { functions_runtime_version_collection = [{ items = [{ id = "version" }] }] } + } + + override_data { + target = data.oci_limits_limit_values.connectors + values = { limit_values = [{ value = 100 }] } + } +} + +variables { + tenancy_ocid = "ocid1.tenancy.oc1..root" + region = "uk-london-1" + betterstack_ingesting_host = "s1.eu-nbg-2.betterstackdata.com" + betterstack_source_token = "token" +} + +run "compartments_1_namespaces_25" { + command = plan + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [] } + } + + assert { + condition = length(oci_sch_service_connector.metrics) == 1 + error_message = "Expected 1 metrics connectors." + } +} + +run "compartments_2_namespaces_25" { + command = plan + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }] } + } + + assert { + condition = length(oci_sch_service_connector.metrics) == 1 + error_message = "Expected 1 metrics connectors." + } +} + +run "compartments_3_namespaces_25" { + command = plan + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }, { id = "ocid1.compartment.oc1..c2", name = "c2" }] } + } + + assert { + condition = length(oci_sch_service_connector.metrics) == 2 + error_message = "Expected 2 metrics connectors." + } +} + +run "compartments_10_namespaces_25" { + command = plan + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }, { id = "ocid1.compartment.oc1..c2", name = "c2" }, { id = "ocid1.compartment.oc1..c3", name = "c3" }, { id = "ocid1.compartment.oc1..c4", name = "c4" }, { id = "ocid1.compartment.oc1..c5", name = "c5" }, { id = "ocid1.compartment.oc1..c6", name = "c6" }, { id = "ocid1.compartment.oc1..c7", name = "c7" }, { id = "ocid1.compartment.oc1..c8", name = "c8" }, { id = "ocid1.compartment.oc1..c9", name = "c9" }] } + } + + assert { + condition = length(oci_sch_service_connector.metrics) == 5 + error_message = "Expected 5 metrics connectors." + } +} + +run "compartments_1_namespaces_10" { + command = plan + + variables { + metrics_namespaces = ["oci_blockstore", "oci_computeagent", "oci_faas", "oci_lbaas", "oci_nat_gateway", "oci_nlb", "oci_objectstorage", "oci_oke", "oci_vcn", "oracle_oci_database"] + } + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [] } + } + + assert { + condition = length(oci_sch_service_connector.metrics) == 1 + error_message = "Expected 1 metrics connectors." + } +} + +run "compartments_2_namespaces_10" { + command = plan + + variables { + metrics_namespaces = ["oci_blockstore", "oci_computeagent", "oci_faas", "oci_lbaas", "oci_nat_gateway", "oci_nlb", "oci_objectstorage", "oci_oke", "oci_vcn", "oracle_oci_database"] + } + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }] } + } + + assert { + condition = length(oci_sch_service_connector.metrics) == 1 + error_message = "Expected 1 metrics connectors." + } +} + +run "compartments_3_namespaces_10" { + command = plan + + variables { + metrics_namespaces = ["oci_blockstore", "oci_computeagent", "oci_faas", "oci_lbaas", "oci_nat_gateway", "oci_nlb", "oci_objectstorage", "oci_oke", "oci_vcn", "oracle_oci_database"] + } + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }, { id = "ocid1.compartment.oc1..c2", name = "c2" }] } + } + + assert { + condition = length(oci_sch_service_connector.metrics) == 1 + error_message = "Expected 1 metrics connectors." + } +} + +run "compartments_10_namespaces_10" { + command = plan + + variables { + metrics_namespaces = ["oci_blockstore", "oci_computeagent", "oci_faas", "oci_lbaas", "oci_nat_gateway", "oci_nlb", "oci_objectstorage", "oci_oke", "oci_vcn", "oracle_oci_database"] + } + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }, { id = "ocid1.compartment.oc1..c2", name = "c2" }, { id = "ocid1.compartment.oc1..c3", name = "c3" }, { id = "ocid1.compartment.oc1..c4", name = "c4" }, { id = "ocid1.compartment.oc1..c5", name = "c5" }, { id = "ocid1.compartment.oc1..c6", name = "c6" }, { id = "ocid1.compartment.oc1..c7", name = "c7" }, { id = "ocid1.compartment.oc1..c8", name = "c8" }, { id = "ocid1.compartment.oc1..c9", name = "c9" }] } + } + + assert { + condition = length(oci_sch_service_connector.metrics) == 2 + error_message = "Expected 2 metrics connectors." + } +} + +run "logs_only" { + command = plan + + variables { + metrics_namespaces = [] + } + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }, { id = "ocid1.compartment.oc1..c2", name = "c2" }] } + } + + assert { + condition = length(oci_sch_service_connector.metrics) == 0 && length(oci_sch_service_connector.logs) == 1 + error_message = "Expected the logs connector only." + } +} + +run "over_the_connector_limit" { + command = plan + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }, { id = "ocid1.compartment.oc1..c2", name = "c2" }, { id = "ocid1.compartment.oc1..c3", name = "c3" }] } + } + + override_data { + target = data.oci_limits_limit_values.connectors + values = { limit_values = [{ value = 2 }] } + } + + expect_failures = [oci_identity_policy.connectors] +} diff --git a/variables.tf b/variables.tf index bd74f70..9c8eaa5 100644 --- a/variables.tf +++ b/variables.tf @@ -24,11 +24,12 @@ variable "betterstack_source_token" { variable "metrics_compartment_ids" { type = string default = "" - description = "Compartment OCIDs to forward metrics from, separated by commas or new lines. Leave empty for every compartment in the tenancy." + description = "Compartment OCIDs to forward metrics from, separated by commas or new lines. Leave empty for every compartment that exists when you apply. Apply again after you add compartments." } variable "metrics_namespaces" { - type = list(string) + type = list(string) + description = "Metric namespaces to forward from every compartment, including services you start using later. Fewer namespaces need fewer Connector Hubs." default = [ "oci_autonomous_database", "oci_blockstore", From a11b2e8793efaaaa4f0d9dd8bec8152223245416 Mon Sep 17 00:00:00 2001 From: Arsen Shkrumelyak Date: Thu, 12 Nov 2026 20:08:03 +0000 Subject: [PATCH 2/8] fix(stack): skip the connector limit check when no limit is returned --- main.tf | 5 ++-- tests/connectors.tftest.hcl | 53 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/main.tf b/main.tf index dec1fec..1825f19 100644 --- a/main.tf +++ b/main.tf @@ -63,7 +63,7 @@ resource "oci_identity_policy" "connectors" { "Allow any-user to use fn-invocation in compartment id ${oci_identity_compartment.betterstack.id} ${local.connector_principal}", ]) - # The policy comes before every connector, so this stops Plan before anything changes. + # A failed precondition stops Plan, so nothing changes. lifecycle { # ponytail: compares against the whole regional limit and ignores Connector Hubs the tenancy already uses elsewhere; subtract the used count (oci_limits_resource_availability) if that bites. precondition { @@ -206,7 +206,8 @@ locals { min(5, floor(50 / length(var.metrics_namespaces))), ) connector_count = length(local.metric_connectors) + (var.forward_audit_logs || length(local.log_group_ids) > 0 ? 1 : 0) - connector_limit = data.oci_limits_limit_values.connectors.limit_values[0].value + # With no limit returned, the check compares the count with itself and passes. + connector_limit = coalesce(try(data.oci_limits_limit_values.connectors.limit_values[0].value, null), local.connector_count) } data "oci_limits_limit_values" "connectors" { diff --git a/tests/connectors.tftest.hcl b/tests/connectors.tftest.hcl index e685b35..06d5357 100644 --- a/tests/connectors.tftest.hcl +++ b/tests/connectors.tftest.hcl @@ -188,3 +188,56 @@ run "over_the_connector_limit" { expect_failures = [oci_identity_policy.connectors] } + +# Root + 1 subcompartment: 1 metrics connector + logs = 2, exactly the free tier's limit. +run "at_the_connector_limit" { + command = plan + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }] } + } + + override_data { + target = data.oci_limits_limit_values.connectors + values = { limit_values = [{ value = 2 }] } + } +} + +# Root + 3 subcompartments without logs: 2 metrics connectors, no logs connector. +run "at_the_connector_limit_without_logs" { + command = plan + + variables { + forward_audit_logs = false + } + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }, { id = "ocid1.compartment.oc1..c2", name = "c2" }, { id = "ocid1.compartment.oc1..c3", name = "c3" }] } + } + + override_data { + target = data.oci_limits_limit_values.connectors + values = { limit_values = [{ value = 2 }] } + } + + assert { + condition = length(oci_sch_service_connector.metrics) == 2 && length(oci_sch_service_connector.logs) == 0 + error_message = "Expected 2 metrics connectors and no logs connector." + } +} + +run "no_connector_limit_returned" { + command = plan + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }, { id = "ocid1.compartment.oc1..c2", name = "c2" }, { id = "ocid1.compartment.oc1..c3", name = "c3" }] } + } + + override_data { + target = data.oci_limits_limit_values.connectors + values = { limit_values = [] } + } +} From b4e75d3fbbe0a17b2f42431c87a1da6166f251d8 Mon Sep 17 00:00:00 2001 From: Arsen Shkrumelyak Date: Thu, 12 Nov 2026 20:08:15 +0000 Subject: [PATCH 3/8] docs: explain the upgrade connector limit and the limits permission --- README.md | 12 ++++++++++-- schema.yaml | 2 +- variables.tf | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1d39e8b..c5a7c88 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Better Stack never connects to your tenancy. Data leaves your tenancy only throu ## Quick start -Before you start, sign in to the **home region** of your tenancy. Use a user who can create compartments and policies in the root compartment. +Before you start, sign in to the **home region** of your tenancy. Use a user who can create compartments and policies in the root compartment, and read service limits (`inspect limits in tenancy`). Administrators can do both. 1. [Create an Oracle Cloud source in Better Stack](https://telemetry.betterstack.com/team/0/sources/new?platform=oracle_cloud). 2. On the **Ingest** tab of the source, click **Deploy to Oracle Cloud**. @@ -29,7 +29,15 @@ The stack sends metrics and logs from the home region only. - One Connector Hub for logs, and one or more Connector Hubs for metrics. Each Connector Hub takes up to 5 compartments and 50 metric namespaces, so it covers 2 compartments with all 25 namespaces selected, and 5 with 10 or fewer. If your region's Connector Hub limit is too low, Plan stops before anything changes. Deselect namespaces, list the compartments to forward metrics from, or request a limit increase. - A policy that lets the Connector Hubs read metrics and logs, and call the Function. -Every selected namespace is forwarded from every compartment that exists when you apply, so services you start using later are forwarded without running Apply again. After you add compartments, apply the stack again. +Every selected namespace is forwarded from each compartment: the ones you list, or every compartment that exists when you apply. Services you start using later are forwarded without running Apply again. After you add compartments, apply the stack again. + +## Upgrade + +1. Go to **Developer Services → Resource Manager → Stacks** and select the Better Stack stack. +2. Click **Edit**, upload `betterstack-oci.zip` from the latest release, and save. +3. Click **Apply**. + +Since v0.1.3, each compartment takes a place on a Connector Hub, even when it has no metrics yet. An upgrade from v0.1.2 can need more Connector Hubs than before. For example, the root compartment and 2 empty subcompartments need 3 on a limit of 2. If Plan stops, deselect namespaces to 10 or fewer, list the compartments to forward metrics from, or request a limit increase. ## Uninstall diff --git a/schema.yaml b/schema.yaml index c597337..ad118a4 100644 --- a/schema.yaml +++ b/schema.yaml @@ -50,7 +50,7 @@ variables: metrics_namespaces: title: Metric namespaces - description: Forwarded from every compartment, including services you start using later. Each Connector Hub covers 2 compartments with all 25 namespaces, and 5 with 10 or fewer. Clear the list to forward logs only. + description: Forwarded from each compartment above, including services you start using later. Each Connector Hub covers 2 compartments with all 25 namespaces, and 5 with 10 or fewer. Clear the list to forward logs only. type: enum additionalProps: allowMultiple: true diff --git a/variables.tf b/variables.tf index 9c8eaa5..578be19 100644 --- a/variables.tf +++ b/variables.tf @@ -29,7 +29,7 @@ variable "metrics_compartment_ids" { variable "metrics_namespaces" { type = list(string) - description = "Metric namespaces to forward from every compartment, including services you start using later. Fewer namespaces need fewer Connector Hubs." + description = "Metric namespaces to forward from each compartment, including services you start using later. Fewer namespaces need fewer Connector Hubs." default = [ "oci_autonomous_database", "oci_blockstore", From 5dedca9852a2acfb48a6594e17ce72fea45741bb Mon Sep 17 00:00:00 2001 From: Arsen Shkrumelyak Date: Thu, 12 Nov 2026 20:12:16 +0000 Subject: [PATCH 4/8] test(stack): count a log-groups-only logs connector against the limit --- tests/connectors.tftest.hcl | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/connectors.tftest.hcl b/tests/connectors.tftest.hcl index 06d5357..ae5e4f5 100644 --- a/tests/connectors.tftest.hcl +++ b/tests/connectors.tftest.hcl @@ -241,3 +241,30 @@ run "no_connector_limit_returned" { values = { limit_values = [] } } } + +# Root + 3 subcompartments with a log group but no audit logs: 2 metrics connectors + logs = 3. +run "over_the_connector_limit_with_log_groups_only" { + command = plan + + variables { + forward_audit_logs = false + log_group_ids = "ocid1.loggroup.oc1..g1" + } + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }, { id = "ocid1.compartment.oc1..c2", name = "c2" }, { id = "ocid1.compartment.oc1..c3", name = "c3" }] } + } + + override_data { + target = data.oci_logging_log_group.selected + values = { compartment_id = "ocid1.compartment.oc1..c1" } + } + + override_data { + target = data.oci_limits_limit_values.connectors + values = { limit_values = [{ value = 2 }] } + } + + expect_failures = [oci_identity_policy.connectors] +} From e3327cd8f7ee37eeb3485d9b55bd75a58f47a43f Mon Sep 17 00:00:00 2001 From: Arsen Shkrumelyak Date: Thu, 12 Nov 2026 20:12:16 +0000 Subject: [PATCH 5/8] docs: say 25 default namespaces instead of all 25 --- README.md | 2 +- schema.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c5a7c88..7582365 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The stack sends metrics and logs from the home region only. - A `betterstack` compartment for all the resources below. - A Function application for the forwarder. It runs in a VCN with a private subnet. A NAT gateway lets the Function reach Better Stack. -- One Connector Hub for logs, and one or more Connector Hubs for metrics. Each Connector Hub takes up to 5 compartments and 50 metric namespaces, so it covers 2 compartments with all 25 namespaces selected, and 5 with 10 or fewer. If your region's Connector Hub limit is too low, Plan stops before anything changes. Deselect namespaces, list the compartments to forward metrics from, or request a limit increase. +- One Connector Hub for logs, and one or more Connector Hubs for metrics. Each Connector Hub takes up to 5 compartments and 50 metric namespaces, so it covers 2 compartments with the 25 default namespaces, and 5 with 10 or fewer. If your region's Connector Hub limit is too low, Plan stops before anything changes. Deselect namespaces, list the compartments to forward metrics from, or request a limit increase. - A policy that lets the Connector Hubs read metrics and logs, and call the Function. Every selected namespace is forwarded from each compartment: the ones you list, or every compartment that exists when you apply. Services you start using later are forwarded without running Apply again. After you add compartments, apply the stack again. diff --git a/schema.yaml b/schema.yaml index ad118a4..5970cc3 100644 --- a/schema.yaml +++ b/schema.yaml @@ -50,7 +50,7 @@ variables: metrics_namespaces: title: Metric namespaces - description: Forwarded from each compartment above, including services you start using later. Each Connector Hub covers 2 compartments with all 25 namespaces, and 5 with 10 or fewer. Clear the list to forward logs only. + description: Forwarded from each compartment above, including services you start using later. Each Connector Hub covers 2 compartments with the 25 default namespaces, and 5 with 10 or fewer. Clear the list to forward logs only. type: enum additionalProps: allowMultiple: true From f5df7efad9a1a1fd2bea27efd16ef512b698b6bc Mon Sep 17 00:00:00 2001 From: Arsen Shkrumelyak Date: Thu, 12 Nov 2026 20:16:08 +0000 Subject: [PATCH 6/8] test(stack): check every namespace per source and the 5-compartment cap --- tests/connectors.tftest.hcl | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/connectors.tftest.hcl b/tests/connectors.tftest.hcl index ae5e4f5..0e4107d 100644 --- a/tests/connectors.tftest.hcl +++ b/tests/connectors.tftest.hcl @@ -268,3 +268,36 @@ run "over_the_connector_limit_with_log_groups_only" { expect_failures = [oci_identity_policy.connectors] } + +run "every_namespace_in_every_compartment" { + command = plan + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }] } + } + + assert { + condition = alltrue([for s in oci_sch_service_connector.metrics[0].source[0].monitoring_sources : length(s.namespace_details[0].namespaces) == length(var.metrics_namespaces)]) && [for s in oci_sch_service_connector.metrics[0].source[0].monitoring_sources : s.compartment_id] == ["ocid1.tenancy.oc1..root", "ocid1.compartment.oc1..c1"] + error_message = "Expected every namespace in root and c1." + } +} + +# 6 compartments × 5 namespaces fit the 50-namespace budget, but a connector takes at most 5 compartments. +run "compartments_6_namespaces_5" { + command = plan + + variables { + metrics_namespaces = ["oci_blockstore", "oci_computeagent", "oci_lbaas", "oci_objectstorage", "oci_vcn"] + } + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }, { id = "ocid1.compartment.oc1..c2", name = "c2" }, { id = "ocid1.compartment.oc1..c3", name = "c3" }, { id = "ocid1.compartment.oc1..c4", name = "c4" }, { id = "ocid1.compartment.oc1..c5", name = "c5" }] } + } + + assert { + condition = length(oci_sch_service_connector.metrics) == 2 + error_message = "Expected 2 metrics connectors." + } +} From ae45d441b23a463a44bff34e6730dd038ec4b5cd Mon Sep 17 00:00:00 2001 From: Arsen Shkrumelyak Date: Thu, 12 Nov 2026 20:16:13 +0000 Subject: [PATCH 7/8] docs: say the limit check ignores Connector Hubs used elsewhere --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7582365..0673621 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The stack sends metrics and logs from the home region only. - A `betterstack` compartment for all the resources below. - A Function application for the forwarder. It runs in a VCN with a private subnet. A NAT gateway lets the Function reach Better Stack. -- One Connector Hub for logs, and one or more Connector Hubs for metrics. Each Connector Hub takes up to 5 compartments and 50 metric namespaces, so it covers 2 compartments with the 25 default namespaces, and 5 with 10 or fewer. If your region's Connector Hub limit is too low, Plan stops before anything changes. Deselect namespaces, list the compartments to forward metrics from, or request a limit increase. +- One Connector Hub for logs, and one or more Connector Hubs for metrics. Each Connector Hub takes up to 5 compartments and 50 metric namespaces, so it covers 2 compartments with the 25 default namespaces, and 5 with 10 or fewer. If your region's Connector Hub limit is too low, Plan stops before anything changes, but it doesn't count Connector Hubs you already use elsewhere in the region. Deselect namespaces, list the compartments to forward metrics from, or request a limit increase. - A policy that lets the Connector Hubs read metrics and logs, and call the Function. Every selected namespace is forwarded from each compartment: the ones you list, or every compartment that exists when you apply. Services you start using later are forwarded without running Apply again. After you add compartments, apply the stack again. From 4faa8210eeb82b3344fc4d3faf3ff45e31ccd704 Mon Sep 17 00:00:00 2001 From: Arsen Shkrumelyak Date: Thu, 12 Nov 2026 20:20:22 +0000 Subject: [PATCH 8/8] test(stack): check listed compartments replace the tenancy-wide list --- tests/connectors.tftest.hcl | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/connectors.tftest.hcl b/tests/connectors.tftest.hcl index 0e4107d..f961f30 100644 --- a/tests/connectors.tftest.hcl +++ b/tests/connectors.tftest.hcl @@ -301,3 +301,21 @@ run "compartments_6_namespaces_5" { error_message = "Expected 2 metrics connectors." } } + +run "listed_compartments_only" { + command = plan + + variables { + metrics_compartment_ids = "ocid1.compartment.oc1..c1, ocid1.compartment.oc1..c2" + } + + override_data { + target = data.oci_identity_compartments.all + values = { compartments = [{ id = "ocid1.compartment.oc1..c1", name = "c1" }, { id = "ocid1.compartment.oc1..c2", name = "c2" }, { id = "ocid1.compartment.oc1..c3", name = "c3" }] } + } + + assert { + condition = length(oci_sch_service_connector.metrics) == 1 && [for s in oci_sch_service_connector.metrics[0].source[0].monitoring_sources : s.compartment_id] == ["ocid1.compartment.oc1..c1", "ocid1.compartment.oc1..c2"] + error_message = "Expected 1 metrics connector with c1 and c2 only." + } +}