Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions docs/resources/telemetry_drain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
---
layout: "heroku"
page_title: "Heroku: heroku_telemetry_drain"
sidebar_current: "docs-heroku-resource-telemetry-drain"
description: |-
Provides a Heroku Telemetry Drain resource.
---

# heroku\_telemetry\_drain

Provides a [Heroku Telemetry Drain](https://devcenter.heroku.com/articles/platform-api-reference#telemetry-drain) resource.

A telemetry drain forwards OpenTelemetry traces, metrics, and logs from Fir generation apps and spaces to your own consumer endpoint.

## Generation Compatibility

Telemetry drains are **only supported for Fir generation** apps and spaces. Cedar generation apps should use the [`heroku_drain`](./drain.html) resource for log forwarding instead.

If you attempt to create a telemetry drain for a Cedar generation app or space, the provider will return a clear error message directing you to use the appropriate resource type.

## Example Usage

### App-scoped Telemetry Drain

```hcl
resource "heroku_space" "fir_space" {
name = "my-fir-space"
organization = "my-org"
region = "virginia"
generation = "fir"
}

resource "heroku_app" "fir_app" {
name = "my-fir-app"
region = "virginia"
space = heroku_space.fir_space.name

organization {
name = "my-org"
}
}

resource "heroku_telemetry_drain" "app_traces" {
owner_id = heroku_app.fir_app.id
owner_type = "app"
endpoint = "https://api.honeycomb.io/v1/traces"
exporter_type = "otlphttp"
signals = ["traces", "metrics"]

headers = {
"x-honeycomb-team" = var.honeycomb_api_key
"x-honeycomb-dataset" = "my-service"
}
}
```

### Space-scoped Telemetry Drain

```hcl
resource "heroku_telemetry_drain" "space_observability" {
owner_id = heroku_space.fir_space.id
owner_type = "space"
endpoint = "otel-collector.example.com:4317"
exporter_type = "otlp"
signals = ["traces", "metrics", "logs"]

headers = {
"Authorization" = "Bearer ${var.collector_token}"
}
}
```

### Logs-only Telemetry Drain

```hcl
resource "heroku_telemetry_drain" "app_logs" {
owner_id = heroku_app.fir_app.id
owner_type = "app"
endpoint = "https://logs.datadog.com/api/v2/logs"
exporter_type = "otlphttp"
signals = ["logs"]

headers = {
"DD-API-KEY" = var.datadog_api_key
}
}
```

## Argument Reference

The following arguments are supported:

* `owner_id` - (Required, ForceNew) The UUID of the app or space that owns this telemetry drain.
* `owner_type` - (Required, ForceNew) The type of owner. Must be either `"app"` or `"space"`.
* `endpoint` - (Required) The URI of your OpenTelemetry consumer endpoint.
* `exporter_type` - (Required) The transport type for your OpenTelemetry consumer. Must be either:
* `"otlphttp"` - HTTP/HTTPS endpoints (e.g., `https://api.example.com/v1/traces`)
* `"otlp"` - gRPC endpoints in `host:port` format (e.g., `collector.example.com:4317`)
* `signals` - (Required) A set of OpenTelemetry signals to send to the telemetry drain. Valid values are:
* `"traces"` - Distributed tracing data
* `"metrics"` - Application and system metrics
* `"logs"` - Application and system logs
* `headers` - (Optional) A map of headers to send to your OpenTelemetry consumer for authentication or configuration.

## Attributes Reference

The following attributes are exported:

* `id` - The UUID of the telemetry drain.
* `created_at` - The timestamp when the telemetry drain was created.
* `updated_at` - The timestamp when the telemetry drain was last updated.

## Endpoint Format Requirements

The `endpoint` format depends on the `exporter_type`:

* **otlphttp**: Full HTTP/HTTPS URL (e.g., `https://api.honeycomb.io/v1/traces`)
* **otlp**: Host and port only (e.g., `collector.example.com:4317`)

## Headers

The `headers` field supports custom key-value pairs for authentication and configuration:

* **Keys**: Must match the pattern `^[A-Za-z0-9\-_]{1,100}$` (alphanumeric, hyphens, underscores, max 100 chars)
* **Values**: Maximum 1000 characters each
* **Limit**: Maximum 20 header pairs per telemetry drain

Common header patterns:
* **API Keys**: `"Authorization" = "Bearer token"` or `"x-api-key" = "key"`
* **Content Types**: `"Content-Type" = "application/x-protobuf"`
* **Service Tags**: `"x-service" = "my-app"`, `"x-environment" = "production"`

## Validation

The provider performs generation-aware validation:

1. **Plan-time**: Schema validation for field types, required fields, and enum values
2. **Apply-time**: Generation compatibility check via Heroku API
* Fetches app/space information to determine generation
* Returns descriptive error if Cedar generation detected
* Suggests using `heroku_drain` for Cedar apps

## Import

Telemetry drains can be imported using the drain `id`:

```
$ terraform import heroku_telemetry_drain.example 01234567-89ab-cdef-0123-456789abcdef
```

## Notes

* **Immutable Owner**: The `owner_id` and `owner_type` cannot be changed after creation (ForceNew)
* **Updates Supported**: `endpoint`, `exporter_type`, `signals`, and `headers` can be modified
* **Generation Requirement**: Only works with Fir generation apps and spaces
* **Multiple Drains**: You can create multiple telemetry drains per app or space
* **Signal Filtering**: Use different drains to send different signal types to different endpoints
12 changes: 8 additions & 4 deletions heroku/heroku_supported_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@ var featureMatrix = map[string]map[string]map[string]bool{
"private_vpn": true,
"outbound_rules": true,
"private_space_logging": true,
"outbound_ips": true, // Cedar supports outbound IPs
"vpn_connection": true, // Cedar supports VPN connections
"inbound_ruleset": true, // Cedar supports inbound rulesets
"peering_connection": true, // Cedar supports IPv4 peering
"outbound_ips": true, // Cedar supports outbound IPs
"vpn_connection": true, // Cedar supports VPN connections
"inbound_ruleset": true, // Cedar supports inbound rulesets
"peering_connection": true, // Cedar supports IPv4 peering
"otel": false, // Cedar doesn't supports OTel at the space level
},
"app": {
"buildpacks": true, // Cedar supports traditional buildpacks
"stack": true, // Cedar supports stack configuration
"internal_routing": true, // Cedar supports internal routing
"cloud_native_buildpacks": false, // Cedar doesn't use CNB by default
"otel": false, // Cedar doesn't supports OTel at the app level
},
"drain": {
"app_log_drains": true, // Cedar supports traditional log drains
Expand All @@ -42,12 +44,14 @@ var featureMatrix = map[string]map[string]map[string]bool{
"vpn_connection": false, // VPN connections not supported
"inbound_ruleset": false, // Inbound rulesets not supported
"peering_connection": false, // IPv4 peering not supported
"otel": true, // Fir supports OTel at the space level
},
"app": {
"buildpacks": false, // Fir doesn't support traditional buildpacks
"stack": false, // Fir doesn't use traditional stack config
"internal_routing": false, // Fir doesn't support internal routing
"cloud_native_buildpacks": true, // Fir uses CNB exclusively
"otel": true, // Fir supports OTel at the app level
},
"drain": {
"app_log_drains": false, // Fir apps don't support traditional log drains
Expand Down
1 change: 1 addition & 0 deletions heroku/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ func Provider() *schema.Provider {
"heroku_space_peering_connection_accepter": resourceHerokuSpacePeeringConnectionAccepter(),
"heroku_space_vpn_connection": resourceHerokuSpaceVPNConnection(),
"heroku_ssl": resourceHerokuSSL(),
"heroku_telemetry_drain": resourceHerokuTelemetryDrain(),
"heroku_team_collaborator": resourceHerokuTeamCollaborator(),
"heroku_team_member": resourceHerokuTeamMember(),
},
Expand Down
19 changes: 19 additions & 0 deletions heroku/resource_heroku_drain.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ func resourceHerokuDrainCreate(d *schema.ResourceData, meta interface{}) error {

appID := d.Get("app_id").(string)

// Check if app supports traditional drains (Cedar generation only)
if err := validateAppSupportsTraditionalDrains(client, appID); err != nil {
return err
}

var url string
if v, ok := d.GetOk("url"); ok {
vs := v.(string)
Expand Down Expand Up @@ -196,6 +201,20 @@ func resourceHerokuDrainRead(d *schema.ResourceData, meta interface{}) error {
return nil
}

// validateAppSupportsTraditionalDrains checks if the app supports traditional log drains (Cedar generation only)
func validateAppSupportsTraditionalDrains(client *heroku.Service, appID string) error {
app, err := client.AppInfo(context.TODO(), appID)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be passing real context from somewhere?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jttyeung this provider is still using original, very old Provider architecture.

Terraform Plugin SDKv2, as used today, does support a context aware interface., but that is not pulled in here yet.

Johnny & I discussed working on SDK/Framework uplift, which will include addressing proper context passthrough, after this initial push to deliver Fir support is complete.

if err != nil {
return fmt.Errorf("error fetching app info: %s", err)
}

if IsFeatureSupported(app.Generation.Name, "app", "otel") {
return fmt.Errorf("traditional log drains are not supported for Fir generation apps. App '%s' is %s generation. Use heroku_telemetry_drain for Fir apps", app.Name, app.Generation.Name)
}

return nil
}

func resourceHerokuDrainV0() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
Expand Down
2 changes: 2 additions & 0 deletions heroku/resource_heroku_space_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ func TestAccHerokuSpace_Fir(t *testing.T) {
testStep_AccHerokuBuild_Generation_FirValid(spaceConfig, spaceName),
// Step 4: Test Fir build generation behavior (invalid build with buildpacks)
testStep_AccHerokuBuild_Generation_FirInvalid(spaceConfig, spaceName),
// Step 5: Test Fir telemetry drain functionality
testStep_AccHerokuTelemetryDrain_Generation_Fir(t, spaceConfig, spaceName),
},
})
}
Expand Down
Loading
Loading