Skip to content

Commit 8b9cf18

Browse files
committed
Add new heroku_telemetry_drain resource for fir generation
1 parent 7669514 commit 8b9cf18

File tree

7 files changed

+596
-4
lines changed

7 files changed

+596
-4
lines changed

docs/resources/telemetry_drain.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
---
2+
layout: "heroku"
3+
page_title: "Heroku: heroku_telemetry_drain"
4+
sidebar_current: "docs-heroku-resource-telemetry-drain"
5+
description: |-
6+
Provides a Heroku Telemetry Drain resource.
7+
---
8+
9+
# heroku\_telemetry\_drain
10+
11+
Provides a [Heroku Telemetry Drain](https://devcenter.heroku.com/articles/platform-api-reference#telemetry-drain) resource.
12+
13+
A telemetry drain forwards OpenTelemetry traces, metrics, and logs from Fir generation apps and spaces to your own consumer endpoint.
14+
15+
## Generation Compatibility
16+
17+
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.
18+
19+
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.
20+
21+
## Example Usage
22+
23+
### App-scoped Telemetry Drain
24+
25+
```hcl
26+
resource "heroku_space" "fir_space" {
27+
name = "my-fir-space"
28+
organization = "my-org"
29+
region = "virginia"
30+
generation = "fir"
31+
}
32+
33+
resource "heroku_app" "fir_app" {
34+
name = "my-fir-app"
35+
region = "virginia"
36+
space = heroku_space.fir_space.name
37+
38+
organization {
39+
name = "my-org"
40+
}
41+
}
42+
43+
resource "heroku_telemetry_drain" "app_traces" {
44+
owner_id = heroku_app.fir_app.id
45+
owner_type = "app"
46+
endpoint = "https://api.honeycomb.io/v1/traces"
47+
exporter_type = "otlphttp"
48+
signals = ["traces", "metrics"]
49+
50+
headers = {
51+
"x-honeycomb-team" = var.honeycomb_api_key
52+
"x-honeycomb-dataset" = "my-service"
53+
}
54+
}
55+
```
56+
57+
### Space-scoped Telemetry Drain
58+
59+
```hcl
60+
resource "heroku_telemetry_drain" "space_observability" {
61+
owner_id = heroku_space.fir_space.id
62+
owner_type = "space"
63+
endpoint = "otel-collector.example.com:4317"
64+
exporter_type = "otlp"
65+
signals = ["traces", "metrics", "logs"]
66+
67+
headers = {
68+
"Authorization" = "Bearer ${var.collector_token}"
69+
}
70+
}
71+
```
72+
73+
### Logs-only Telemetry Drain
74+
75+
```hcl
76+
resource "heroku_telemetry_drain" "app_logs" {
77+
owner_id = heroku_app.fir_app.id
78+
owner_type = "app"
79+
endpoint = "https://logs.datadog.com/api/v2/logs"
80+
exporter_type = "otlphttp"
81+
signals = ["logs"]
82+
83+
headers = {
84+
"DD-API-KEY" = var.datadog_api_key
85+
}
86+
}
87+
```
88+
89+
## Argument Reference
90+
91+
The following arguments are supported:
92+
93+
* `owner_id` - (Required, ForceNew) The UUID of the app or space that owns this telemetry drain.
94+
* `owner_type` - (Required, ForceNew) The type of owner. Must be either `"app"` or `"space"`.
95+
* `endpoint` - (Required) The URI of your OpenTelemetry consumer endpoint.
96+
* `exporter_type` - (Required) The transport type for your OpenTelemetry consumer. Must be either:
97+
* `"otlphttp"` - HTTP/HTTPS endpoints (e.g., `https://api.example.com/v1/traces`)
98+
* `"otlp"` - gRPC endpoints in `host:port` format (e.g., `collector.example.com:4317`)
99+
* `signals` - (Required) A set of OpenTelemetry signals to send to the telemetry drain. Valid values are:
100+
* `"traces"` - Distributed tracing data
101+
* `"metrics"` - Application and system metrics
102+
* `"logs"` - Application and system logs
103+
* `headers` - (Optional) A map of headers to send to your OpenTelemetry consumer for authentication or configuration.
104+
105+
## Attributes Reference
106+
107+
The following attributes are exported:
108+
109+
* `id` - The UUID of the telemetry drain.
110+
* `created_at` - The timestamp when the telemetry drain was created.
111+
* `updated_at` - The timestamp when the telemetry drain was last updated.
112+
113+
## Endpoint Format Requirements
114+
115+
The `endpoint` format depends on the `exporter_type`:
116+
117+
* **otlphttp**: Full HTTP/HTTPS URL (e.g., `https://api.honeycomb.io/v1/traces`)
118+
* **otlp**: Host and port only (e.g., `collector.example.com:4317`)
119+
120+
## Headers
121+
122+
The `headers` field supports custom key-value pairs for authentication and configuration:
123+
124+
* **Keys**: Must match the pattern `^[A-Za-z0-9\-_]{1,100}$` (alphanumeric, hyphens, underscores, max 100 chars)
125+
* **Values**: Maximum 1000 characters each
126+
* **Limit**: Maximum 20 header pairs per telemetry drain
127+
128+
Common header patterns:
129+
* **API Keys**: `"Authorization" = "Bearer token"` or `"x-api-key" = "key"`
130+
* **Content Types**: `"Content-Type" = "application/x-protobuf"`
131+
* **Service Tags**: `"x-service" = "my-app"`, `"x-environment" = "production"`
132+
133+
## Validation
134+
135+
The provider performs generation-aware validation:
136+
137+
1. **Plan-time**: Schema validation for field types, required fields, and enum values
138+
2. **Apply-time**: Generation compatibility check via Heroku API
139+
* Fetches app/space information to determine generation
140+
* Returns descriptive error if Cedar generation detected
141+
* Suggests using `heroku_drain` for Cedar apps
142+
143+
## Import
144+
145+
Telemetry drains can be imported using the drain `id`:
146+
147+
```
148+
$ terraform import heroku_telemetry_drain.example 01234567-89ab-cdef-0123-456789abcdef
149+
```
150+
151+
## Notes
152+
153+
* **Immutable Owner**: The `owner_id` and `owner_type` cannot be changed after creation (ForceNew)
154+
* **Updates Supported**: `endpoint`, `exporter_type`, `signals`, and `headers` can be modified
155+
* **Generation Requirement**: Only works with Fir generation apps and spaces
156+
* **Multiple Drains**: You can create multiple telemetry drains per app or space
157+
* **Signal Filtering**: Use different drains to send different signal types to different endpoints

heroku/heroku_supported_features.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,18 @@ var featureMatrix = map[string]map[string]map[string]bool{
1515
"private_vpn": true,
1616
"outbound_rules": true,
1717
"private_space_logging": true,
18-
"outbound_ips": true, // Cedar supports outbound IPs
19-
"vpn_connection": true, // Cedar supports VPN connections
20-
"inbound_ruleset": true, // Cedar supports inbound rulesets
21-
"peering_connection": true, // Cedar supports IPv4 peering
18+
"outbound_ips": true, // Cedar supports outbound IPs
19+
"vpn_connection": true, // Cedar supports VPN connections
20+
"inbound_ruleset": true, // Cedar supports inbound rulesets
21+
"peering_connection": true, // Cedar supports IPv4 peering
22+
"otel": false, // Cedar doesn't supports OTel at the space level
2223
},
2324
"app": {
2425
"buildpacks": true, // Cedar supports traditional buildpacks
2526
"stack": true, // Cedar supports stack configuration
2627
"internal_routing": true, // Cedar supports internal routing
2728
"cloud_native_buildpacks": false, // Cedar doesn't use CNB by default
29+
"otel": false, // Cedar doesn't supports OTel at the app level
2830
},
2931
"drain": {
3032
"app_log_drains": true, // Cedar supports traditional log drains
@@ -42,12 +44,14 @@ var featureMatrix = map[string]map[string]map[string]bool{
4244
"vpn_connection": false, // VPN connections not supported
4345
"inbound_ruleset": false, // Inbound rulesets not supported
4446
"peering_connection": false, // IPv4 peering not supported
47+
"otel": true, // Fir supports OTel at the space level
4548
},
4649
"app": {
4750
"buildpacks": false, // Fir doesn't support traditional buildpacks
4851
"stack": false, // Fir doesn't use traditional stack config
4952
"internal_routing": false, // Fir doesn't support internal routing
5053
"cloud_native_buildpacks": true, // Fir uses CNB exclusively
54+
"otel": true, // Fir supports OTel at the app level
5155
},
5256
"drain": {
5357
"app_log_drains": false, // Fir apps don't support traditional log drains

heroku/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ func Provider() *schema.Provider {
127127
"heroku_space_peering_connection_accepter": resourceHerokuSpacePeeringConnectionAccepter(),
128128
"heroku_space_vpn_connection": resourceHerokuSpaceVPNConnection(),
129129
"heroku_ssl": resourceHerokuSSL(),
130+
"heroku_telemetry_drain": resourceHerokuTelemetryDrain(),
130131
"heroku_team_collaborator": resourceHerokuTeamCollaborator(),
131132
"heroku_team_member": resourceHerokuTeamMember(),
132133
},

heroku/resource_heroku_drain.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ func resourceHerokuDrainCreate(d *schema.ResourceData, meta interface{}) error {
119119

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

122+
// Check if app supports traditional drains (Cedar generation only)
123+
if err := validateAppSupportsTraditionalDrains(client, appID); err != nil {
124+
return err
125+
}
126+
122127
var url string
123128
if v, ok := d.GetOk("url"); ok {
124129
vs := v.(string)
@@ -196,6 +201,20 @@ func resourceHerokuDrainRead(d *schema.ResourceData, meta interface{}) error {
196201
return nil
197202
}
198203

204+
// validateAppSupportsTraditionalDrains checks if the app supports traditional log drains (Cedar generation only)
205+
func validateAppSupportsTraditionalDrains(client *heroku.Service, appID string) error {
206+
app, err := client.AppInfo(context.TODO(), appID)
207+
if err != nil {
208+
return fmt.Errorf("error fetching app info: %s", err)
209+
}
210+
211+
if IsFeatureSupported(app.Generation.Name, "app", "otel") {
212+
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)
213+
}
214+
215+
return nil
216+
}
217+
199218
func resourceHerokuDrainV0() *schema.Resource {
200219
return &schema.Resource{
201220
Schema: map[string]*schema.Schema{

heroku/resource_heroku_space_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ func TestAccHerokuSpace_Fir(t *testing.T) {
9090
testStep_AccHerokuBuild_Generation_FirValid(spaceConfig, spaceName),
9191
// Step 4: Test Fir build generation behavior (invalid build with buildpacks)
9292
testStep_AccHerokuBuild_Generation_FirInvalid(spaceConfig, spaceName),
93+
// Step 5: Test Fir telemetry drain functionality
94+
testStep_AccHerokuTelemetryDrain_Generation_Fir(t, spaceConfig, spaceName),
9395
},
9496
})
9597
}

0 commit comments

Comments
 (0)