diff --git a/CHANGELOG.md b/CHANGELOG.md index 80668c7a..86559ca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v2.2.0 (unreleased) + +FEATURES: +- Support collecting telemetry data. + ## v2.1.0 FEATURES: diff --git a/internal/context/context.go b/internal/context/context.go index 7db6cf5f..130e33a4 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -5,6 +5,7 @@ import ( "github.com/Azure/azapi-lsp/internal/filesystem" "github.com/Azure/azapi-lsp/internal/langserver/diagnostics" "github.com/Azure/azapi-lsp/internal/langserver/session" + "github.com/Azure/azapi-lsp/internal/telemetry" ) type contextKey struct { @@ -20,6 +21,7 @@ var ( ctxDiagsNotifier = &contextKey{"diagnostics notifier"} ctxLsVersion = &contextKey{"language server version"} ctxClientCaller = &contextKey{Name: "client caller"} + ctxTelemetry = &contextKey{"telemetry"} ) func missingContextErr(ctxKey *contextKey) *MissingContextErr { @@ -89,3 +91,16 @@ func ClientNotifier(ctx context.Context) (session.ClientNotifier, error) { return clientNotifier, nil } + +func WithTelemetry(ctx context.Context, telemetry telemetry.Sender) context.Context { + return context.WithValue(ctx, ctxTelemetry, telemetry) +} + +func Telemetry(ctx context.Context) (telemetry.Sender, error) { + tel, ok := ctx.Value(ctxTelemetry).(telemetry.Sender) + if !ok { + return nil, missingContextErr(ctxTelemetry) + } + + return tel, nil +} diff --git a/internal/langserver/handlers/command/arm_template_converter.go b/internal/langserver/handlers/command/arm_template_converter.go index 31ba39e1..a116b999 100644 --- a/internal/langserver/handlers/command/arm_template_converter.go +++ b/internal/langserver/handlers/command/arm_template_converter.go @@ -1,10 +1,12 @@ package command import ( + "context" "encoding/json" "fmt" "strings" + "github.com/Azure/azapi-lsp/internal/telemetry" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclwrite" @@ -69,7 +71,7 @@ func (c *Context) String() string { return result } -func convertARMTemplate(input string) (string, error) { +func convertARMTemplate(ctx context.Context, input string, telemetrySender telemetry.Sender) (string, error) { var model ARMTemplateModel err := json.Unmarshal([]byte(input), &model) if err != nil { @@ -94,7 +96,17 @@ func convertARMTemplate(input string) (string, error) { c.AppendBlock(varBlock) } + typeSet := make(map[string]bool) for _, resource := range model.Resources { + typeValue := "" + if resource["type"] != nil { + typeValue = resource["type"].(string) + } + if resource["apiVersion"] != nil { + typeValue = fmt.Sprintf("%s@%s", typeValue, resource["apiVersion"].(string)) + } + typeSet[typeValue] = true + res := flattenARMExpression(resource) data, err := json.MarshalIndent(res, "", " ") if err != nil { @@ -111,6 +123,16 @@ func convertARMTemplate(input string) (string, error) { c.AppendBlock(resourceBlock) } + types := make([]string, 0) + for t := range typeSet { + types = append(types, t) + } + telemetrySender.SendEvent(ctx, "ConvertJsonToAzapi", map[string]interface{}{ + "status": "completed", + "kind": "arm-template", + "type": strings.Join(types, ","), + }) + return c.String(), nil } diff --git a/internal/langserver/handlers/command/aztfmigrate_command.go b/internal/langserver/handlers/command/aztfmigrate_command.go index 31b5a927..8de48706 100644 --- a/internal/langserver/handlers/command/aztfmigrate_command.go +++ b/internal/langserver/handlers/command/aztfmigrate_command.go @@ -16,6 +16,7 @@ import ( lsctx "github.com/Azure/azapi-lsp/internal/context" ilsp "github.com/Azure/azapi-lsp/internal/lsp" lsp "github.com/Azure/azapi-lsp/internal/protocol" + "github.com/Azure/azapi-lsp/internal/utils" "github.com/Azure/aztfmigrate/azurerm" "github.com/Azure/aztfmigrate/tf" "github.com/Azure/aztfmigrate/types" @@ -43,6 +44,11 @@ func (c AztfMigrateCommand) Handle(ctx context.Context, arguments []json.RawMess } } + telemetrySender, err := context2.Telemetry(ctx) + if err != nil { + return nil, err + } + clientCaller, err := context2.ClientCaller(ctx) if err != nil { return nil, err @@ -53,6 +59,9 @@ func (c AztfMigrateCommand) Handle(ctx context.Context, arguments []json.RawMess return nil, err } + telemetrySender.SendEvent(ctx, "aztfmigrate", map[string]interface{}{ + "status": "started", + }) reportProgress(ctx, "Parsing Terraform configurations...", 0) defer reportProgress(ctx, "Migration completed.", 100) @@ -173,6 +182,9 @@ func (c AztfMigrateCommand) Handle(ctx context.Context, arguments []json.RawMess allResources := types.ListResourcesFromPlan(plan) resources := make([]types.AzureResource, 0) + + srcAzapiTypes := make(map[string]bool) + srcAzurermTypes := make(map[string]bool) for _, r := range allResources { if syntaxBlockMap[r.OldAddress(nil)] == nil { continue @@ -199,11 +211,15 @@ func (c AztfMigrateCommand) Handle(ctx context.Context, arguments []json.RawMess resource.ResourceType = resourceTypes[0] resources = append(resources, resource) + azureResourceType := utils.GetResourceType(resourceId) + srcAzapiTypes[azureResourceType] = true + case *types.AzurermResource: if len(resource.Instances) == 0 { continue } resources = append(resources, resource) + srcAzurermTypes[resource.OldResourceType] = true } } @@ -311,6 +327,21 @@ func (c AztfMigrateCommand) Handle(ctx context.Context, arguments []json.RawMess }, }) + azapiTypes := make([]string, 0) + for t := range srcAzapiTypes { + azapiTypes = append(azapiTypes, t) + } + azurermTypes := make([]string, 0) + for t := range srcAzurermTypes { + azurermTypes = append(azurermTypes, t) + } + telemetrySender.SendEvent(ctx, "aztfmigrate", map[string]interface{}{ + "status": "completed", + "count": fmt.Sprintf("%d", len(resources)), + "azapi": strings.Join(azapiTypes, ","), + "azurerm": strings.Join(azurermTypes, ","), + }) + return nil, nil } diff --git a/internal/langserver/handlers/command/convert_json_command.go b/internal/langserver/handlers/command/convert_json_command.go index b7c41d1b..de05cee0 100644 --- a/internal/langserver/handlers/command/convert_json_command.go +++ b/internal/langserver/handlers/command/convert_json_command.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + + lsctx "github.com/Azure/azapi-lsp/internal/context" pluralize "github.com/gertd/go-pluralize" ) @@ -25,21 +27,44 @@ func (c ConvertJsonCommand) Handle(ctx context.Context, arguments []json.RawMess return nil, nil } + telemetrySender, err := lsctx.Telemetry(ctx) + if err != nil { + return nil, err + } + var model map[string]interface{} - err := json.Unmarshal([]byte(content), &model) + err = json.Unmarshal([]byte(content), &model) if err != nil { return nil, fmt.Errorf("unable to unmarshal JSON content: %w", err) } result := "" if model["$schema"] != nil { - result, err = convertARMTemplate(content) + telemetrySender.SendEvent(ctx, "ConvertJsonToAzapi", map[string]interface{}{ + "status": "started", + "kind": "arm-template", + }) + result, err = convertARMTemplate(ctx, content, telemetrySender) if err != nil { + telemetrySender.SendEvent(ctx, "ConvertJsonToAzapi", map[string]interface{}{ + "status": "failed", + "kind": "arm-template", + "error": err.Error(), + }) return nil, err } } else { - result, err = convertResourceJson(content) + telemetrySender.SendEvent(ctx, "ConvertJsonToAzapi", map[string]interface{}{ + "status": "started", + "kind": "resource-json", + }) + result, err = convertResourceJson(ctx, content, telemetrySender) if err != nil { + telemetrySender.SendEvent(ctx, "ConvertJsonToAzapi", map[string]interface{}{ + "status": "failed", + "kind": "resource-json", + "error": err.Error(), + }) return nil, err } } diff --git a/internal/langserver/handlers/command/resource_json_converter.go b/internal/langserver/handlers/command/resource_json_converter.go index e7f1f341..bf611847 100644 --- a/internal/langserver/handlers/command/resource_json_converter.go +++ b/internal/langserver/handlers/command/resource_json_converter.go @@ -1,6 +1,7 @@ package command import ( + "context" "encoding/json" "fmt" "log" @@ -8,6 +9,7 @@ import ( "github.com/Azure/azapi-lsp/internal/azure" "github.com/Azure/azapi-lsp/internal/azure/types" + "github.com/Azure/azapi-lsp/internal/telemetry" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclwrite" @@ -29,7 +31,7 @@ type identityModel struct { UserAssignedIdentities map[string]interface{} `json:"userAssignedIdentities"` } -func convertResourceJson(input string) (string, error) { +func convertResourceJson(ctx context.Context, input string, telemetrySender telemetry.Sender) (string, error) { var model resourceModel err := json.Unmarshal([]byte(input), &model) if err != nil { @@ -52,6 +54,12 @@ func convertResourceJson(input string) (string, error) { apiVersion = parts[1] } + telemetrySender.SendEvent(ctx, "ConvertJsonToAzapi", map[string]interface{}{ + "status": "completed", + "kind": "resource-json", + "type": typeValue, + }) + importBlock := hclwrite.NewBlock("import", nil) importBlock.Body().SetAttributeValue("id", cty.StringVal(fmt.Sprintf("%s?api-version=%s", model.ID, apiVersion))) importBlock.Body().SetAttributeTraversal("to", hcl.Traversal{hcl.TraverseRoot{Name: "azapi_resource"}, hcl.TraverseAttr{Name: label}}) diff --git a/internal/langserver/handlers/command/telemetry_command.go b/internal/langserver/handlers/command/telemetry_command.go new file mode 100644 index 00000000..92fbe73c --- /dev/null +++ b/internal/langserver/handlers/command/telemetry_command.go @@ -0,0 +1,30 @@ +package command + +import ( + "context" + "encoding/json" + "fmt" + lsctx "github.com/Azure/azapi-lsp/internal/context" + lsp "github.com/Azure/azapi-lsp/internal/protocol" +) + +type TelemetryCommand struct { +} + +func (t TelemetryCommand) Handle(ctx context.Context, arguments []json.RawMessage) (interface{}, error) { + telemetrySender, err := lsctx.Telemetry(ctx) + if err != nil { + return nil, err + } + + var params lsp.TelemetryEvent + if len(arguments) != 0 { + err := json.Unmarshal(arguments[0], ¶ms) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal arguments: %w", err) + } + } + + telemetrySender.SendEvent(ctx, params.Name, params.Properties) + return nil, nil +} diff --git a/internal/langserver/handlers/complete.go b/internal/langserver/handlers/complete.go index fcf1c7f3..75ff6b00 100644 --- a/internal/langserver/handlers/complete.go +++ b/internal/langserver/handlers/complete.go @@ -43,6 +43,13 @@ func (svc *service) TextDocumentComplete(ctx context.Context, params lsp.Complet IsIncomplete: false, Items: complete.CandidatesAtPos(data, doc.Filename(), fPos.Position(), svc.logger), } - svc.logger.Printf("received candidates: %#v", candidates) + + if len(candidates.Items) > 0 { + telemetrySender, err := lsctx.Telemetry(ctx) + if err != nil { + return candidates, err + } + telemetrySender.SendEvent(ctx, "textDocument/completion", nil) + } return candidates, err } diff --git a/internal/langserver/handlers/execute_command.go b/internal/langserver/handlers/execute_command.go index c64c3386..e11d963b 100644 --- a/internal/langserver/handlers/execute_command.go +++ b/internal/langserver/handlers/execute_command.go @@ -12,15 +12,17 @@ var handlerMap = map[string]command.CommandHandler{} const CommandConvertJsonToAzapi = "azapi.convertJsonToAzapi" const CommandAztfMigrate = "azapi.aztfmigrate" +const CommandTelemetry = "azapi.telemetry" func availableCommands() []string { - return []string{CommandConvertJsonToAzapi, CommandAztfMigrate} + return []string{CommandConvertJsonToAzapi, CommandAztfMigrate, CommandTelemetry} } func init() { handlerMap = make(map[string]command.CommandHandler) handlerMap[CommandConvertJsonToAzapi] = command.ConvertJsonCommand{} handlerMap[CommandAztfMigrate] = command.AztfMigrateCommand{} + handlerMap[CommandTelemetry] = command.TelemetryCommand{} } func (svc *service) WorkspaceExecuteCommand(ctx context.Context, params lsp.ExecuteCommandParams) (interface{}, error) { diff --git a/internal/langserver/handlers/handlers_test.go b/internal/langserver/handlers/handlers_test.go index 338e7ff7..88851fb5 100644 --- a/internal/langserver/handlers/handlers_test.go +++ b/internal/langserver/handlers/handlers_test.go @@ -43,7 +43,8 @@ func initializeResponse(t *testing.T, commandPrefix string) string { "executeCommandProvider": { "commands": [ "azapi.convertJsonToAzapi", - "azapi.aztfmigrate" + "azapi.aztfmigrate", + "azapi.telemetry" ], "workDoneProgress": true } diff --git a/internal/langserver/handlers/hover.go b/internal/langserver/handlers/hover.go index c8e7a18b..8e2b2d9f 100644 --- a/internal/langserver/handlers/hover.go +++ b/internal/langserver/handlers/hover.go @@ -3,9 +3,8 @@ package handlers import ( "context" - "github.com/Azure/azapi-lsp/internal/langserver/handlers/hover" - lsctx "github.com/Azure/azapi-lsp/internal/context" + "github.com/Azure/azapi-lsp/internal/langserver/handlers/hover" ilsp "github.com/Azure/azapi-lsp/internal/lsp" lsp "github.com/Azure/azapi-lsp/internal/protocol" ) @@ -36,9 +35,18 @@ func (svc *service) TextDocumentHover(ctx context.Context, params lsp.TextDocume return nil, err } + telemetrySender, err := lsctx.Telemetry(ctx) + if err != nil { + return nil, err + } + svc.logger.Printf("Looking for hover data at %q -> %#v", doc.Filename(), fPos.Position()) - hoverData := hover.HoverAtPos(data, doc.Filename(), fPos.Position(), svc.logger) + hoverData := hover.HoverAtPos(ctx, data, doc.Filename(), fPos.Position(), svc.logger, telemetrySender) svc.logger.Printf("received hover data: %#v", hoverData) + if hoverData != nil { + telemetrySender.SendEvent(ctx, "textDocument/hover", nil) + } + return hoverData, nil } diff --git a/internal/langserver/handlers/hover/hover.go b/internal/langserver/handlers/hover/hover.go index 48835be8..943e9a89 100644 --- a/internal/langserver/handlers/hover/hover.go +++ b/internal/langserver/handlers/hover/hover.go @@ -1,6 +1,7 @@ package hover import ( + "context" "fmt" "log" "strings" @@ -12,11 +13,12 @@ import ( ilsp "github.com/Azure/azapi-lsp/internal/lsp" "github.com/Azure/azapi-lsp/internal/parser" lsp "github.com/Azure/azapi-lsp/internal/protocol" + "github.com/Azure/azapi-lsp/internal/telemetry" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" ) -func HoverAtPos(data []byte, filename string, pos hcl.Pos, logger *log.Logger) *lsp.Hover { +func HoverAtPos(ctx context.Context, data []byte, filename string, pos hcl.Pos, logger *log.Logger, sender telemetry.Sender) *lsp.Hover { file, _ := hclsyntax.ParseConfig(data, filename, hcl.InitialPos) body, isHcl := file.Body.(*hclsyntax.Body) if !isHcl { @@ -108,6 +110,10 @@ func HoverAtPos(data []byte, filename string, pos hcl.Pos, logger *log.Logger) * if azureResourceType == "" { return nil } + sender.SendEvent(ctx, "textDocument/hover", map[string]interface{}{ + "kind": "resource-definition", + "type": typeValue, + }) return &lsp.Hover{ Range: ilsp.HCLRangeToLSP(block.DefRange()), Contents: lsp.MarkupContent{ diff --git a/internal/langserver/handlers/service.go b/internal/langserver/handlers/service.go index 91df8d85..53857fe1 100644 --- a/internal/langserver/handlers/service.go +++ b/internal/langserver/handlers/service.go @@ -146,6 +146,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = ilsp.WithClientCapabilities(ctx, cc) + ctx = lsctx.WithTelemetry(ctx, svc.telemetry) return handle(ctx, req, svc.TextDocumentComplete) }, @@ -158,6 +159,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { ctx = lsctx.WithDocumentStorage(ctx, svc.fs) ctx = ilsp.WithClientCapabilities(ctx, cc) ctx = ilsp.ContextWithClientName(ctx, &clientName) + ctx = lsctx.WithTelemetry(ctx, svc.telemetry) return handle(ctx, req, svc.TextDocumentHover) }, @@ -181,6 +183,8 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { ctx = lsctx.WithClientCaller(ctx, svc.clientCaller) ctx = lsctx.WithClientNotifier(ctx, svc.clientNotifier) ctx = lsctx.WithDocumentStorage(ctx, svc.fs) + ctx = lsctx.WithTelemetry(ctx, svc.telemetry) + return handle(ctx, req, svc.WorkspaceExecuteCommand) }, "shutdown": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { diff --git a/internal/langserver/handlers/snippets/snippets.go b/internal/langserver/handlers/snippets/snippets.go index 77d73052..e9c7e58c 100644 --- a/internal/langserver/handlers/snippets/snippets.go +++ b/internal/langserver/handlers/snippets/snippets.go @@ -1,6 +1,7 @@ package snippets import ( + "encoding/json" "fmt" "log" "strings" @@ -80,6 +81,16 @@ func CodeSampleCandidates(block *hclsyntax.Block, editRange lsp.Range) []lsp.Com if newText == "" { return nil } + + event := lsp.TelemetryEvent{ + Version: lsp.TelemetryFormatVersion, + Name: "textDocument/completion", + Properties: map[string]interface{}{ + "kind": "code-sample", + "type": typeValue, + }, + } + data, _ := json.Marshal(event) return []lsp.CompletionItem{ { Label: "code sample", @@ -96,6 +107,11 @@ func CodeSampleCandidates(block *hclsyntax.Block, editRange lsp.Range) []lsp.Com Range: editRange, NewText: newText, }, + Command: &lsp.Command{ + Title: "", + Command: "azapi.telemetry", + Arguments: []json.RawMessage{data}, + }, }, } } diff --git a/internal/langserver/handlers/snippets/templates.go b/internal/langserver/handlers/snippets/templates.go index 9400dde3..7f82a54b 100644 --- a/internal/langserver/handlers/snippets/templates.go +++ b/internal/langserver/handlers/snippets/templates.go @@ -46,6 +46,16 @@ func TemplateCandidates(editRange lsp.Range) []lsp.CompletionItem { } for _, template := range templates { + event := lsp.TelemetryEvent{ + Version: lsp.TelemetryFormatVersion, + Name: "textDocument/completion", + Properties: map[string]interface{}{ + "kind": "template", + "label": template.Label, + }, + } + data, _ := json.Marshal(event) + templateCandidates = append(templateCandidates, lsp.CompletionItem{ Label: template.Label, Kind: lsp.SnippetCompletion, @@ -61,6 +71,11 @@ func TemplateCandidates(editRange lsp.Range) []lsp.CompletionItem { Range: editRange, NewText: template.TextEdit.NewText, }, + Command: &lsp.Command{ + Title: "", + Command: "azapi.telemetry", + Arguments: []json.RawMessage{data}, + }, }) } return templateCandidates diff --git a/internal/langserver/handlers/testdata/TestCompletion_codeSample/expect.json b/internal/langserver/handlers/testdata/TestCompletion_codeSample/expect.json index 52e05ef8..0e5b2f77 100644 --- a/internal/langserver/handlers/testdata/TestCompletion_codeSample/expect.json +++ b/internal/langserver/handlers/testdata/TestCompletion_codeSample/expect.json @@ -28,6 +28,20 @@ } }, "newText": "parent_id = ${1:\"The id of the Microsoft.Resources/resourceGroups@2020-06-01 resource\"}\nname = \"${2:The name of the resource}\"\nlocation = \"${3:location}\"\nbody = {\n properties = {\n internet = \"Disabled\"\n managementCluster = {\n clusterSize = 3\n }\n networkBlock = \"192.168.48.0/22\"\n }\n sku = {\n name = \"av36\"\n }\n }\nschema_validation_enabled = false\nresponse_export_values = [\"*\"]\n" + }, + "command": { + "title": "", + "command": "azapi.telemetry", + "arguments": [ + { + "v": 1, + "name": "textDocument/completion", + "properties": { + "kind": "code-sample", + "type": "Microsoft.AVS/privateClouds@2023-03-01" + } + } + ] } }, { diff --git a/internal/langserver/handlers/testdata/TestExecuteCommand_basic/expect.tf b/internal/langserver/handlers/testdata/TestExecuteCommand_basic/expect.tf index 4ef9be0e..2dabccf8 100644 --- a/internal/langserver/handlers/testdata/TestExecuteCommand_basic/expect.tf +++ b/internal/langserver/handlers/testdata/TestExecuteCommand_basic/expect.tf @@ -1,17 +1,17 @@ /* Note: This is a generated HCL content from the JSON input which is based on the latest API version available. To import the resource, please run the following command: -terraform import azapi_resource.managedCluster /subscriptions/0000000/resourcegroups/acctestRG-aks-240626160102776968/providers/Microsoft.ContainerService/managedClusters/acctestaks240626160102776968?api-version=2024-08-01 +terraform import azapi_resource.managedCluster /subscriptions/0000000/resourcegroups/acctestRG-aks-240626160102776968/providers/Microsoft.ContainerService/managedClusters/acctestaks240626160102776968?api-version=2024-09-02-preview Or add the below config: import { - id = "/subscriptions/0000000/resourcegroups/acctestRG-aks-240626160102776968/providers/Microsoft.ContainerService/managedClusters/acctestaks240626160102776968?api-version=2024-08-01" + id = "/subscriptions/0000000/resourcegroups/acctestRG-aks-240626160102776968/providers/Microsoft.ContainerService/managedClusters/acctestaks240626160102776968?api-version=2024-09-02-preview" to = azapi_resource.managedCluster } */ resource "azapi_resource" "managedCluster" { - type = "Microsoft.ContainerService/managedClusters@2024-08-01" + type = "Microsoft.ContainerService/managedClusters@2024-09-02-preview" parent_id = "/subscriptions/0000000/resourceGroups/acctestRG-aks-240626160102776968" name = "acctestaks240626160102776968" location = "eastus" diff --git a/internal/langserver/handlers/testdata/TestExecuteCommand_nestedResource/expect.tf b/internal/langserver/handlers/testdata/TestExecuteCommand_nestedResource/expect.tf index c63f01be..167912b0 100644 --- a/internal/langserver/handlers/testdata/TestExecuteCommand_nestedResource/expect.tf +++ b/internal/langserver/handlers/testdata/TestExecuteCommand_nestedResource/expect.tf @@ -1,17 +1,17 @@ /* Note: This is a generated HCL content from the JSON input which is based on the latest API version available. To import the resource, please run the following command: -terraform import azapi_resource.subnet /subscriptions/0000000/resourceGroups/acctestRG-databricks-240530173443928447/providers/Microsoft.Network/virtualNetworks/acctest-vnet-240530173443928447/subnets/acctest-sn-private-240530173443928447?api-version=2024-03-01 +terraform import azapi_resource.subnet /subscriptions/0000000/resourceGroups/acctestRG-databricks-240530173443928447/providers/Microsoft.Network/virtualNetworks/acctest-vnet-240530173443928447/subnets/acctest-sn-private-240530173443928447?api-version=2024-05-01 Or add the below config: import { - id = "/subscriptions/0000000/resourceGroups/acctestRG-databricks-240530173443928447/providers/Microsoft.Network/virtualNetworks/acctest-vnet-240530173443928447/subnets/acctest-sn-private-240530173443928447?api-version=2024-03-01" + id = "/subscriptions/0000000/resourceGroups/acctestRG-databricks-240530173443928447/providers/Microsoft.Network/virtualNetworks/acctest-vnet-240530173443928447/subnets/acctest-sn-private-240530173443928447?api-version=2024-05-01" to = azapi_resource.subnet } */ resource "azapi_resource" "subnet" { - type = "Microsoft.Network/virtualNetworks/subnets@2024-03-01" + type = "Microsoft.Network/virtualNetworks/subnets@2024-05-01" parent_id = "/subscriptions/0000000/resourceGroups/acctestRG-databricks-240530173443928447/providers/Microsoft.Network/virtualNetworks/acctest-vnet-240530173443928447" name = "acctest-sn-private-240530173443928447" body = { diff --git a/internal/telemetry/noop.go b/internal/telemetry/noop.go index 1422a954..d00dd510 100644 --- a/internal/telemetry/noop.go +++ b/internal/telemetry/noop.go @@ -6,6 +6,8 @@ import ( "log" ) +var _ Sender = &NoopSender{} + type NoopSender struct { Logger *log.Logger } diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go index 34b675b1..44f1e7ea 100644 --- a/internal/telemetry/telemetry.go +++ b/internal/telemetry/telemetry.go @@ -3,10 +3,13 @@ package telemetry import ( "context" "fmt" + "time" lsp "github.com/Azure/azapi-lsp/internal/protocol" ) +var _ Sender = &Telemetry{} + type Telemetry struct { version int notifier Notifier @@ -28,9 +31,92 @@ func NewSender(version int, notifier Notifier) (*Telemetry, error) { } func (t *Telemetry) SendEvent(ctx context.Context, name string, properties map[string]interface{}) { + for _, limiter := range telemetryRateLimiters { + if !limiter.Allow(name, properties) { + return + } + } + _ = t.notifier.Notify(ctx, "telemetry/event", lsp.TelemetryEvent{ Version: t.version, Name: name, Properties: properties, }) } + +var telemetryRateLimiters []TelemetryRateLimiter + +func init() { + telemetryRateLimiters = make([]TelemetryRateLimiter, 0) + telemetryRateLimiters = append(telemetryRateLimiters, &HoverTelemetryRateLimiter{}) + telemetryRateLimiters = append(telemetryRateLimiters, &CompletionTelemetryRateLimiter{}) +} + +type TelemetryRateLimiter interface { + Allow(name string, properties map[string]interface{}) bool +} + +var _ TelemetryRateLimiter = &HoverTelemetryRateLimiter{} + +type HoverTelemetryRateLimiter struct { + lastSentTimeStamp int64 +} + +func (h *HoverTelemetryRateLimiter) Allow(name string, properties map[string]interface{}) bool { + if name != "textDocument/hover" { + return true + } + // always allow the resource-definition hover + if properties != nil && properties["kind"] != nil { + if kind, ok := properties["kind"].(string); ok && kind == "resource-definition" { + return true + } + } + + timeStampNow := time.Now().Unix() + if h.lastSentTimeStamp == 0 { + h.lastSentTimeStamp = timeStampNow + return true + } + + // minimum interval between two hover telemetry events is 1 hour + if timeStampNow-h.lastSentTimeStamp < 3600 { + return false + } + h.lastSentTimeStamp = timeStampNow + return true +} + +var _ TelemetryRateLimiter = &CompletionTelemetryRateLimiter{} + +type CompletionTelemetryRateLimiter struct { + lastSentTimeStamp int64 +} + +func (c *CompletionTelemetryRateLimiter) Allow(name string, properties map[string]interface{}) bool { + if name != "textDocument/completion" { + return true + } + + // always allow code-sample and template completions + if properties != nil && properties["kind"] != nil { + if kind, ok := properties["kind"].(string); ok { + if kind == "code-sample" || kind == "template" { + return true + } + } + } + + timeStampNow := time.Now().Unix() + if c.lastSentTimeStamp == 0 { + c.lastSentTimeStamp = timeStampNow + return true + } + + // minimum interval between two completion telemetry events is 1 hour + if timeStampNow-c.lastSentTimeStamp < 3600 { + return false + } + c.lastSentTimeStamp = timeStampNow + return true +} diff --git a/internal/telemetry/telemetry_test.go b/internal/telemetry/telemetry_test.go new file mode 100644 index 00000000..02505750 --- /dev/null +++ b/internal/telemetry/telemetry_test.go @@ -0,0 +1,86 @@ +package telemetry + +import ( + "testing" + + lsp "github.com/Azure/azapi-lsp/internal/protocol" +) + +func Test_HoverTelemetryRateLimiter(t *testing.T) { + rateLimiter := HoverTelemetryRateLimiter{} + + inputs := []lsp.TelemetryEvent{ + { + Name: "textDocument/hover", + }, + { + Name: "textDocument/completion", + }, + { + Name: "textDocument/hover", + Properties: make(map[string]interface{}), + }, + { + Name: "textDocument/hover", + Properties: map[string]interface{}{ + "kind": "resource-definition", + }, + }, + } + + expected := []bool{ + true, + true, + false, + true, + } + + for i, input := range inputs { + if got := rateLimiter.Allow(input.Name, input.Properties); got != expected[i] { + t.Errorf("for input no. %d, got %v, want %v", i, got, expected[i]) + } + } +} + +func Test_CompletionTelemetryRateLimiter(t *testing.T) { + rateLimiter := CompletionTelemetryRateLimiter{} + + inputs := []lsp.TelemetryEvent{ + { + Name: "textDocument/completion", + }, + { + Name: "textDocument/hover", + }, + { + Name: "textDocument/completion", + Properties: make(map[string]interface{}), + }, + { + Name: "textDocument/completion", + Properties: map[string]interface{}{ + "kind": "code-sample", + }, + }, + { + Name: "textDocument/completion", + Properties: map[string]interface{}{ + "kind": "template", + }, + }, + } + + expected := []bool{ + true, + true, + false, + true, + true, + } + + for i, input := range inputs { + if got := rateLimiter.Allow(input.Name, input.Properties); got != expected[i] { + t.Errorf("for input no. %d, got %v, want %v", i, got, expected[i]) + } + } +}