Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement opt-in telemetry #681

Merged
merged 3 commits into from
Oct 26, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ Please follow the [relevant guide for your IDE](./docs/USAGE.md).
- [Martin Atkins](https://github.com/apparentlymart) - particularly the virtual filesystem
- [Zhe Cheng](https://github.com/njuCZ) - research, design, prototyping assistance
- [Julio Sueiras](https://github.com/juliosueiras) - particularly his [language server implementation](https://github.com/juliosueiras/terraform-lsp)


## Telemetry

The server will collect data only if the _client_ requests it during initialization. Telemetry is opt-in by default.

[Read more about telemetry](./docs/telemetry.md).

## `terraform-ls` VS `terraform-lsp`

Expand Down
4 changes: 4 additions & 0 deletions docs/language-clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,7 @@ Clients are encouraged to implement custom commands
in a command palette or similar functionality.

See [./commands.md](./commands.md) for more.

## Telemetry

See [./telemetry.md](./telemetry.md).
88 changes: 88 additions & 0 deletions docs/telemetry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Telemetry

The language server is capable of sending telemetry using the native LSP `telemetry/event` method.
Telemetry is off by default and can enabled by passing a supported request format version
as an experimental client capability.

```json
{
"capabilities": {
"experimental": {
"telemetryVersion": 1
}
}
}
```

Clients then implement opt-in or opt-out in the UI and should reflect the user's choice.

## Privacy

Sensitive data, such as filesystem paths or addresses of providers sourced from outside the Terraform Registry
are anonymized. Random UUID is generated in memory and tracked instead of a path or a private provider address.

Mapping of such UUIDs is not persisted anywhere other than in memory during process lifetime.

## Request Format

The only supported version is currently `1`. Version negotiation allows the server
to introduce breaking changes to the format and have clients adopt gradually.

### `v1`

[`telemetry/event`](https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#telemetry_event) structure

```json
{
"v": 1,
"name": "eventName",
"properties": {}
}
```

`properties` may contain **any (valid) JSON types**
including arrays and arbitrarily nested objects. It is client's
reponsibility to serialize these properties when and if necessary.

Example events:

```json
{
"v": 1,
"name": "initialize",
"properties": {
"experimentalCapabilities.referenceCountCodeLens": true,
"lsVersion": "0.23.0",
"options.commandPrefix": true,
"options.excludeModulePaths": false,
"options.experimentalFeatures.prefillRequiredFields": false,
"options.experimentalFeatures.validateOnSave": false,
"options.rootModulePaths": false,
"options.terraformExecPath": false,
"options.terraformExecTimeout": "",
"options.terraformLogFilePath": false,
"root_uri": "dir"
}
}
```
```json
{
"v": 1,
"name": "moduleData",
"properties": {
"backend": "remote",
"backend.remote.hostname": "app.terraform.io",
"installedProviders": {
"registry.terraform.io/hashicorp/aws": "3.57.0",
"registry.terraform.io/hashicorp/null": "3.1.0"
},
"moduleId": "8aa5a4dc-4780-2d90-b8fb-57de8288fb32",
"providerRequirements": {
"registry.terraform.io/hashicorp/aws": "",
"registry.terraform.io/hashicorp/null": "~> 3.1"
},
"tfRequirements": "~> 1.0",
"tfVersion": "1.0.7"
}
}
```
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ require (
github.com/hashicorp/go-getter v1.5.9
github.com/hashicorp/go-memdb v1.3.2
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-uuid v1.0.1
github.com/hashicorp/go-version v1.3.0
github.com/hashicorp/hcl-lang v0.0.0-20211014152429-0bfbdcca0902
github.com/hashicorp/hcl/v2 v2.10.1
github.com/hashicorp/terraform-exec v0.15.0
github.com/hashicorp/terraform-json v0.13.0
github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045
github.com/hashicorp/terraform-schema v0.0.0-20210823185306-e7a9c4e84cd1
github.com/hashicorp/terraform-schema v0.0.0-20211021151419-21dfff199031
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5
github.com/mitchellh/cli v1.1.2
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -201,14 +201,13 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/terraform-exec v0.15.0 h1:cqjh4d8HYNQrDoEmlSGelHmg2DYDh5yayckvJ5bV18E=
github.com/hashicorp/terraform-exec v0.15.0/go.mod h1:H4IG8ZxanU+NW0ZpDRNsvh9f0ul7C0nHP+rUR/CHs7I=
github.com/hashicorp/terraform-json v0.12.0/go.mod h1:pmbq9o4EuL43db5+0ogX10Yofv1nozM+wskr/bGFJpI=
github.com/hashicorp/terraform-json v0.13.0 h1:Li9L+lKD1FO5RVFRM1mMMIBDoUHslOniyEi5CM+FWGY=
github.com/hashicorp/terraform-json v0.13.0/go.mod h1:y5OdLBCT+rxbwnpxZs9kGL7R9ExU76+cpdY8zHwoazk=
github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896/go.mod h1:bzBPnUIkI0RxauU8Dqo+2KrZZ28Cf48s8V6IHt3p4co=
github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045 h1:R/I8ofvXuPcTNoc//N4ruvaHGZcShI/VuU2iXo875Lo=
github.com/hashicorp/terraform-registry-address v0.0.0-20210816115301-cb2034eba045/go.mod h1:anRyJbe12BZscpFgaeGu9gH12qfdBP094LYFtuAFzd4=
github.com/hashicorp/terraform-schema v0.0.0-20210823185306-e7a9c4e84cd1 h1:K6wkKTi4+aSYXDFGbGWgd3sP+gWTTM9VYOVeCXjJJm8=
github.com/hashicorp/terraform-schema v0.0.0-20210823185306-e7a9c4e84cd1/go.mod h1:wG+IttAk2LqgHE76fD0wt2kucaKzV7pzm7OTqCJJC3M=
github.com/hashicorp/terraform-schema v0.0.0-20211021151419-21dfff199031 h1:HwQTGktZUBlRENcwb9MKm+cfqNcv0C5vagJnjKAqNKY=
github.com/hashicorp/terraform-schema v0.0.0-20211021151419-21dfff199031/go.mod h1:DlxWg9rEgltUs+FD5ElEgBoP985cjAeA9YHcYliAGVg=
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0=
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
Expand Down Expand Up @@ -381,7 +380,6 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.2.1/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
github.com/zclconf/go-cty v1.9.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
github.com/zclconf/go-cty v1.9.1 h1:viqrgQwFl5UpSxc046qblj78wZXVDFnSOufaOTER+cc=
Expand Down
87 changes: 87 additions & 0 deletions internal/langserver/handlers/hooks_module.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package handlers

import (
"context"
"fmt"

"github.com/hashicorp/terraform-ls/internal/state"
"github.com/hashicorp/terraform-ls/internal/telemetry"
"github.com/hashicorp/terraform-schema/backend"
)

func sendModuleTelemetry(ctx context.Context, store *state.StateStore, telemetrySender telemetry.Sender) state.ModuleChangeHook {
return func(_, newMod *state.Module) {
modId, err := store.GetModuleID(newMod.Path)
if err != nil {
return
}

properties := map[string]interface{}{
"moduleId": modId,
}

if len(newMod.Meta.CoreRequirements) > 0 {
properties["tfRequirements"] = newMod.Meta.CoreRequirements.String()
}
if newMod.Meta.Backend != nil {
properties["backend"] = newMod.Meta.Backend.Type
if data, ok := newMod.Meta.Backend.Data.(*backend.Remote); ok {
hostname := data.Hostname

// anonymize any non-default hostnames
if hostname != "" && hostname != "app.terraform.io" {
hostname = "custom-hostname"
}

properties["backend.remote.hostname"] = hostname
}
}
if len(newMod.Meta.ProviderRequirements) > 0 {
reqs := make(map[string]string, 0)
for pAddr, cons := range newMod.Meta.ProviderRequirements {
if telemetry.IsPublicProvider(pAddr) {
reqs[pAddr.String()] = cons.String()
continue
}

// anonymize any unknown providers or the ones not publicly listed
id, err := store.GetProviderID(pAddr)
if err != nil {
continue
}
addr := fmt.Sprintf("unlisted/%s", id)
reqs[addr] = cons.String()
}
properties["providerRequirements"] = reqs
}

if newMod.TerraformVersion != nil {
properties["tfVersion"] = newMod.TerraformVersion.String()
}

if len(newMod.InstalledProviders) > 0 {
installedProviders := make(map[string]string, 0)
for pAddr, pv := range newMod.InstalledProviders {
if telemetry.IsPublicProvider(pAddr) {
versionString := ""
if pv != nil {
versionString = pv.String()
}
installedProviders[pAddr.String()] = versionString
continue
}

// anonymize any unknown providers or the ones not publicly listed
id, err := store.GetProviderID(pAddr)
if err != nil {
continue
}
addr := fmt.Sprintf("unlisted/%s", id)
installedProviders[addr] = ""
}
properties["installedProviders"] = installedProviders
}

telemetrySender.SendEvent(ctx, "moduleData", properties)
}
}
44 changes: 41 additions & 3 deletions internal/langserver/handlers/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,41 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams)
serverCaps.ServerInfo.Version = version
}

clientCaps := params.Capabilities

properties := map[string]interface{}{
radeksimko marked this conversation as resolved.
Show resolved Hide resolved
"experimentalCapabilities.referenceCountCodeLens": false,
"options.rootModulePaths": false,
"options.excludeModulePaths": false,
"options.commandPrefix": false,
"options.experimentalFeatures.validateOnSave": false,
"options.terraformExecPath": false,
"options.terraformExecTimeout": "",
"options.terraformLogFilePath": false,
"root_uri": "dir",
"lsVersion": serverCaps.ServerInfo.Version,
}

expClientCaps := lsp.ExperimentalClientCapabilities(clientCaps.Experimental)

if tv, ok := expClientCaps.TelemetryVersion(); ok {
svc.logger.Printf("enabling telemetry (version: %d)", tv)
err := svc.setupTelemetry(tv, jrpc2.ServerFromContext(ctx))
if err != nil {
svc.logger.Printf("failed to setup telemetry: %s", err)
}
svc.logger.Printf("telemetry enabled (version: %d)", tv)
}
defer svc.telemetry.SendEvent(ctx, "initialize", properties)

fh := ilsp.FileHandlerFromDirURI(params.RootURI)
if fh.URI() == "" || !fh.IsDir() {
properties["root_uri"] = "file"
return serverCaps, fmt.Errorf("Editing a single file is not yet supported." +
" Please open a directory.")
}
if !fh.Valid() {
properties["root_uri"] = "invalid"
return serverCaps, fmt.Errorf("URI %q is not valid", params.RootURI)
}

Expand All @@ -74,12 +103,11 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams)
}
}

clientCaps := params.Capabilities

if _, ok = lsp.ExperimentalClientCapabilities(clientCaps.Experimental).ShowReferencesCommandId(); ok {
if _, ok = expClientCaps.ShowReferencesCommandId(); ok {
serverCaps.Capabilities.Experimental = lsp.ExperimentalServerCapabilities{
ReferenceCountCodeLens: true,
}
properties["experimentalCapabilities.referenceCountCodeLens"] = true
}

err = lsctx.SetClientCapabilities(ctx, &clientCaps)
Expand All @@ -91,6 +119,16 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams)
if err != nil {
return serverCaps, err
}

properties["options.rootModulePaths"] = len(out.Options.ModulePaths) > 0
properties["options.excludeModulePaths"] = len(out.Options.ExcludeModulePaths) > 0
properties["options.commandPrefix"] = len(out.Options.CommandPrefix) > 0
properties["options.experimentalFeatures.prefillRequiredFields"] = out.Options.ExperimentalFeatures.PrefillRequiredFields
properties["options.experimentalFeatures.validateOnSave"] = out.Options.ExperimentalFeatures.ValidateOnSave
properties["options.terraformExecPath"] = len(out.Options.TerraformExecPath) > 0
properties["options.terraformExecTimeout"] = out.Options.TerraformExecTimeout
properties["options.terraformLogFilePath"] = len(out.Options.TerraformLogFilePath) > 0

err = out.Options.Validate()
if err != nil {
return serverCaps, err
Expand Down
17 changes: 17 additions & 0 deletions internal/langserver/handlers/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/hashicorp/terraform-ls/internal/schemas"
"github.com/hashicorp/terraform-ls/internal/settings"
"github.com/hashicorp/terraform-ls/internal/state"
"github.com/hashicorp/terraform-ls/internal/telemetry"
"github.com/hashicorp/terraform-ls/internal/terraform/discovery"
"github.com/hashicorp/terraform-ls/internal/terraform/exec"
"github.com/hashicorp/terraform-ls/internal/terraform/module"
Expand All @@ -48,6 +49,7 @@ type service struct {
tfDiscoFunc discovery.DiscoveryFunc
tfExecFactory exec.ExecutorFactory
tfExecOpts *exec.ExecutorOpts
telemetry telemetry.Sender

additionalHandlers map[string]rpch.Func
}
Expand All @@ -70,6 +72,7 @@ func NewSession(srvCtx context.Context) session.Session {
newWalker: module.NewWalker,
tfDiscoFunc: d.LookPath,
tfExecFactory: exec.NewExecutor,
telemetry: &telemetry.NoopSender{},
}
}

Expand All @@ -89,6 +92,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) {
return nil, fmt.Errorf("Unable to prepare session: %w", err)
}

svc.telemetry = &telemetry.NoopSender{Logger: svc.logger}
svc.fs.SetLogger(svc.logger)

lh := LogHandler(svc.logger)
Expand Down Expand Up @@ -443,6 +447,9 @@ func (svc *service) configureSessionDependencies(cfgOpts *settings.Options) erro
return err
}
store.SetLogger(svc.logger)
store.Modules.ChangeHooks = state.ModuleChangeHooks{
sendModuleTelemetry(svc.sessCtx, store, svc.telemetry),
}

err = schemas.PreloadSchemasToStore(store.ProviderSchemas)
if err != nil {
Expand All @@ -469,6 +476,16 @@ func (svc *service) configureSessionDependencies(cfgOpts *settings.Options) erro
return nil
}

func (svc *service) setupTelemetry(version int, notifier session.ClientNotifier) error {
t, err := telemetry.NewSender(version, notifier)
if err != nil {
return err
}

svc.telemetry = t
return nil
}

func (svc *service) Finish(_ jrpc2.Assigner, status jrpc2.ServerStatus) {
if status.Closed || status.Err != nil {
svc.logger.Printf("session stopped unexpectedly (err: %v)", status.Err)
Expand Down
4 changes: 4 additions & 0 deletions internal/langserver/session/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ type Session interface {
SetLogger(*log.Logger)
}

type ClientNotifier interface {
Notify(ctx context.Context, method string, params interface{}) error
}

type SessionFactory func(context.Context) Session
12 changes: 12 additions & 0 deletions internal/protocol/experimental.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,15 @@ func (cc ExpClientCapabilities) ShowReferencesCommandId() (string, bool) {
cmdId, ok := cc["showReferencesCommandId"].(string)
return cmdId, ok
}

func (cc ExpClientCapabilities) TelemetryVersion() (int, bool) {
if cc == nil {
return 0, false
}

// numbers are unmarshalled as float64 from JSON
// per https://pkg.go.dev/encoding/json#Unmarshal
v, ok := cc["telemetryVersion"].(float64)

return int(v), ok
}
Loading