Skip to content

Commit e2007b4

Browse files
committed
remove service_option_all option
1 parent dca1f8e commit e2007b4

File tree

3 files changed

+132
-50
lines changed

3 files changed

+132
-50
lines changed

docs/data-sources/collector.md

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ Read-Only:
6565
- `memory_batch_size_mb` (Number)
6666
- `namespace_option` (Set of Object) (see [below for nested schema](#nestedobjatt--configuration--namespace_option))
6767
- `service_option` (Set of Object) (see [below for nested schema](#nestedobjatt--configuration--service_option))
68-
- `service_option_all` (Set of Object) (see [below for nested schema](#nestedobjatt--configuration--service_option_all))
6968
- `traces_sample_rate` (Number)
7069
- `vrl_transformation` (String)
7170

@@ -106,16 +105,6 @@ Read-Only:
106105
- `name` (String)
107106

108107

109-
<a id="nestedobjatt--configuration--service_option_all"></a>
110-
### Nested Schema for `configuration.service_option_all`
111-
112-
Read-Only:
113-
114-
- `ingest_traces` (Boolean)
115-
- `log_sampling` (Number)
116-
- `name` (String)
117-
118-
119108

120109
<a id="nestedatt--custom_bucket"></a>
121110
### Nested Schema for `custom_bucket`

docs/resources/collector.md

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,10 @@ Optional:
9898
- `logs_sample_rate` (Number) Sample rate for logs (0-100).
9999
- `memory_batch_size_mb` (Number) Memory batch size in MB for outgoing requests. Maximum 40 MB.
100100
- `namespace_option` (Block Set) Per-namespace overrides for log sampling rate and trace ingestion (Kubernetes only). Order-independent; entries are identified by name. (see [below for nested schema](#nestedblock--configuration--namespace_option))
101-
- `service_option` (Block Set) Per-service overrides for log sampling rate and trace ingestion. Only includes user-managed services; internal collector services (`better-stack-beyla`, `better-stack-collector`) are excluded. See `service_option_all` for the complete server state. (see [below for nested schema](#nestedblock--configuration--service_option))
101+
- `service_option` (Block Set) Per-service overrides for log sampling rate and trace ingestion. Only includes user-managed services; internal collector services (`better-stack-beyla`, `better-stack-collector`) are excluded. Use the `logtail_collector` data source to see all discovered services. (see [below for nested schema](#nestedblock--configuration--service_option))
102102
- `traces_sample_rate` (Number) Sample rate for traces (0-100).
103103
- `vrl_transformation` (String) VRL transformation that runs on the collector host, inside your infrastructure, before data is transmitted to Better Stack. Use this for PII redaction and sensitive data filtering — raw data never leaves your network. For server-side transformations that run during ingestion on Better Stack, use the top-level `source_vrl_transformation` attribute instead. Read more about [VRL transformations](https://betterstack.com/docs/logs/using-logtail/transforming-ingested-data/logs-vrl/).
104104

105-
Read-Only:
106-
107-
- `service_option_all` (Set of Object) All per-service overrides including server-managed internal defaults (`better-stack-beyla`, `better-stack-collector`). Read-only; to configure services, use `service_option`. (see [below for nested schema](#nestedatt--configuration--service_option_all))
108-
109105
<a id="nestedblock--configuration--components"></a>
110106
### Nested Schema for `configuration.components`
111107

@@ -149,16 +145,6 @@ Optional:
149145
- `log_sampling` (Number) Log sampling rate (0-100).
150146

151147

152-
<a id="nestedatt--configuration--service_option_all"></a>
153-
### Nested Schema for `configuration.service_option_all`
154-
155-
Read-Only:
156-
157-
- `ingest_traces` (Boolean)
158-
- `log_sampling` (Number)
159-
- `name` (String)
160-
161-
162148

163149
<a id="nestedblock--custom_bucket"></a>
164150
### Nested Schema for `custom_bucket`

internal/provider/resource_collector.go

Lines changed: 131 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ var collectorSchema = map[string]*schema.Schema{
271271
ValidateFunc: validation.IntAtMost(40),
272272
},
273273
"service_option": {
274-
Description: "Per-service overrides for log sampling rate and trace ingestion. Only includes user-managed services; internal collector services (`better-stack-beyla`, `better-stack-collector`) are excluded. See `service_option_all` for the complete server state.",
274+
Description: "Per-service overrides for log sampling rate and trace ingestion. Only includes user-managed services; internal collector services (`better-stack-beyla`, `better-stack-collector`) are excluded. Use the `logtail_collector` data source to see all discovered services.",
275275
Type: schema.TypeSet,
276276
Optional: true,
277277
Set: hashOptionEntry,
@@ -283,19 +283,6 @@ var collectorSchema = map[string]*schema.Schema{
283283
},
284284
},
285285
},
286-
"service_option_all": {
287-
Description: "All per-service overrides including server-managed internal defaults (`better-stack-beyla`, `better-stack-collector`). Read-only; to configure services, use `service_option`.",
288-
Type: schema.TypeSet,
289-
Computed: true,
290-
Set: hashOptionEntry,
291-
Elem: &schema.Resource{
292-
Schema: map[string]*schema.Schema{
293-
"name": {Type: schema.TypeString, Computed: true, Description: "Service name."},
294-
"log_sampling": {Type: schema.TypeInt, Computed: true, Description: "Log sampling rate (0-100)."},
295-
"ingest_traces": {Type: schema.TypeBool, Computed: true, Description: "Whether to ingest traces for this service."},
296-
},
297-
},
298-
},
299286
"namespace_option": {
300287
Description: "Per-namespace overrides for log sampling rate and trace ingestion (Kubernetes only). Order-independent; entries are identified by name.",
301288
Type: schema.TypeSet,
@@ -581,6 +568,101 @@ func fetchCollectorDatabases(ctx context.Context, meta interface{}, collectorID
581568
return databases, nil
582569
}
583570

571+
// getUserManagedOptionNames reads the current state's service_option or namespace_option
572+
// names before d.Set overwrites them. Returns a map of names the user explicitly manages.
573+
// Returns an empty map for data sources or resources with no option blocks.
574+
func getUserManagedOptionNames(d *schema.ResourceData, key string) map[string]bool {
575+
managed := make(map[string]bool)
576+
configData, ok := d.GetOk("configuration")
577+
if !ok {
578+
return managed
579+
}
580+
configList, ok := configData.([]interface{})
581+
if !ok || len(configList) == 0 {
582+
return managed
583+
}
584+
configMap, ok := configList[0].(map[string]interface{})
585+
if !ok {
586+
return managed
587+
}
588+
optionsSet, ok := configMap[key].(*schema.Set)
589+
if !ok || optionsSet.Len() == 0 {
590+
return managed
591+
}
592+
for _, item := range optionsSet.List() {
593+
entry, ok := item.(map[string]interface{})
594+
if !ok {
595+
continue
596+
}
597+
if name, ok := entry["name"].(string); ok {
598+
managed[name] = true
599+
}
600+
}
601+
return managed
602+
}
603+
604+
// fetchCurrentCollectorConfig fetches the current collector from the API to get
605+
// server-side services_options and namespaces_options for merge during updates.
606+
func fetchCurrentCollectorConfig(ctx context.Context, meta interface{}, collectorID string) (*collectorConfiguration, diag.Diagnostics) {
607+
c := meta.(*client)
608+
res, err := c.Get(ctx, fmt.Sprintf("/api/v1/collectors/%s", url.PathEscape(collectorID)))
609+
if err != nil {
610+
return nil, diag.FromErr(err)
611+
}
612+
defer func() {
613+
_, _ = io.Copy(io.Discard, res.Body)
614+
_ = res.Body.Close()
615+
}()
616+
617+
body, err := io.ReadAll(res.Body)
618+
if res.StatusCode != http.StatusOK {
619+
return nil, diag.Errorf("GET /api/v1/collectors/%s returned %d: %s", collectorID, res.StatusCode, string(body))
620+
}
621+
if err != nil {
622+
return nil, diag.FromErr(err)
623+
}
624+
625+
var out collectorHTTPResponse
626+
if err := json.Unmarshal(body, &out); err != nil {
627+
return nil, diag.FromErr(err)
628+
}
629+
630+
return out.Data.Attributes.Configuration, nil
631+
}
632+
633+
// mergeEntityOptions merges user-managed service/namespace options with server state.
634+
// User entries always win; server entries not in the user's config are preserved
635+
// (i.e. auto-discovered services/namespaces the user doesn't manage).
636+
func mergeEntityOptions(userCfg, serverCfg *collectorConfiguration) {
637+
if serverCfg == nil {
638+
return
639+
}
640+
641+
// Merge services: server entries not in user config are preserved
642+
if serverCfg.ServicesOptions != nil {
643+
if userCfg.ServicesOptions == nil {
644+
userCfg.ServicesOptions = make(map[string]collectorEntityOption)
645+
}
646+
for name, opt := range serverCfg.ServicesOptions {
647+
if _, exists := userCfg.ServicesOptions[name]; !exists {
648+
userCfg.ServicesOptions[name] = opt
649+
}
650+
}
651+
}
652+
653+
// Merge namespaces: server entries not in user config are preserved
654+
if serverCfg.NamespacesOptions != nil {
655+
if userCfg.NamespacesOptions == nil {
656+
userCfg.NamespacesOptions = make(map[string]collectorEntityOption)
657+
}
658+
for name, opt := range serverCfg.NamespacesOptions {
659+
if _, exists := userCfg.NamespacesOptions[name]; !exists {
660+
userCfg.NamespacesOptions[name] = opt
661+
}
662+
}
663+
}
664+
}
665+
584666
func newCollectorResource() *schema.Resource {
585667
return &schema.Resource{
586668
CreateContext: collectorCreate,
@@ -821,9 +903,18 @@ func collectorUpdate(ctx context.Context, d *schema.ResourceData, meta interface
821903
}
822904
}
823905

824-
// Load configuration if changed
906+
// Load configuration if changed — merge with server state to preserve
907+
// unmanaged services/namespaces that were auto-discovered by the collector.
825908
if d.HasChange("configuration") {
826909
in.Configuration = loadCollectorConfiguration(d)
910+
911+
if in.Configuration != nil {
912+
serverCfg, derr := fetchCurrentCollectorConfig(ctx, meta, d.Id())
913+
if derr != nil {
914+
return derr
915+
}
916+
mergeEntityOptions(in.Configuration, serverCfg)
917+
}
827918
}
828919

829920
// Load proxy config if changed (buffering proxy, SSL, HTTP Basic Auth)
@@ -878,6 +969,10 @@ func collectorCopyAttrs(d *schema.ResourceData, in *collector) diag.Diagnostics
878969
}
879970

880971
// Copy configuration — only set the block if there are meaningful (non-default) fields.
972+
// Capture user-managed option names BEFORE d.Set overwrites the state.
973+
managedServices := getUserManagedOptionNames(d, "service_option")
974+
managedNamespaces := getUserManagedOptionNames(d, "namespace_option")
975+
881976
if in.Configuration != nil {
882977
configData := make(map[string]interface{})
883978
if in.Configuration.LogsSampleRate != nil {
@@ -934,17 +1029,27 @@ func collectorCopyAttrs(d *schema.ResourceData, in *collector) diag.Diagnostics
9341029
configData["memory_batch_size_mb"] = *in.Configuration.MemoryBatchSizeMB
9351030
}
9361031

937-
// Copy services_options map → service_option (user-managed, excludes better-stack-* internal
938-
// defaults) and service_option_all (complete server state). Follows the tags/tags_all pattern.
1032+
// Copy services_options map → service_option, filtered to user-managed entries.
1033+
// If the user manages specific services, only those are stored in state (prevents
1034+
// perpetual plan drift from auto-discovered services). If no user management (data
1035+
// source or import), include all non-internal services.
9391036
if in.Configuration.ServicesOptions != nil {
940-
serviceOptionAll := make([]interface{}, 0, len(in.Configuration.ServicesOptions))
9411037
serviceOptionData := make([]interface{}, 0, len(in.Configuration.ServicesOptions))
9421038
names := make([]string, 0, len(in.Configuration.ServicesOptions))
9431039
for name := range in.Configuration.ServicesOptions {
9441040
names = append(names, name)
9451041
}
9461042
sort.Strings(names)
9471043
for _, name := range names {
1044+
// If user manages specific services, only include those.
1045+
// Otherwise (data source / import / no service_option blocks), include all non-internal.
1046+
if len(managedServices) > 0 {
1047+
if !managedServices[name] {
1048+
continue
1049+
}
1050+
} else if strings.HasPrefix(name, "better-stack-") || strings.HasPrefix(name, "better-stack_") {
1051+
continue
1052+
}
9481053
opt := in.Configuration.ServicesOptions[name]
9491054
entry := map[string]interface{}{"name": name}
9501055
if opt.LogSampling != nil {
@@ -953,16 +1058,13 @@ func collectorCopyAttrs(d *schema.ResourceData, in *collector) diag.Diagnostics
9531058
if opt.IngestTraces != nil {
9541059
entry["ingest_traces"] = *opt.IngestTraces
9551060
}
956-
serviceOptionAll = append(serviceOptionAll, entry)
957-
if !strings.HasPrefix(name, "better-stack-") && !strings.HasPrefix(name, "better-stack_") {
958-
serviceOptionData = append(serviceOptionData, entry)
959-
}
1061+
serviceOptionData = append(serviceOptionData, entry)
9601062
}
9611063
configData["service_option"] = serviceOptionData
962-
configData["service_option_all"] = serviceOptionAll
9631064
}
9641065

965-
// Copy namespaces_options map → namespace_option set
1066+
// Copy namespaces_options map → namespace_option, filtered to user-managed entries.
1067+
// Same partial-management logic as service_option above.
9661068
if in.Configuration.NamespacesOptions != nil {
9671069
namespaceOptionData := make([]interface{}, 0, len(in.Configuration.NamespacesOptions))
9681070
names := make([]string, 0, len(in.Configuration.NamespacesOptions))
@@ -971,6 +1073,11 @@ func collectorCopyAttrs(d *schema.ResourceData, in *collector) diag.Diagnostics
9711073
}
9721074
sort.Strings(names)
9731075
for _, name := range names {
1076+
// If user manages specific namespaces, only include those.
1077+
// Otherwise (data source / import), include all.
1078+
if len(managedNamespaces) > 0 && !managedNamespaces[name] {
1079+
continue
1080+
}
9741081
opt := in.Configuration.NamespacesOptions[name]
9751082
entry := map[string]interface{}{"name": name}
9761083
if opt.LogSampling != nil {

0 commit comments

Comments
 (0)