Skip to content

Improving Logging/Tracing/Observability #440

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

Merged
merged 16 commits into from
Sep 16, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,4 @@ jobs:
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: Test Coverage
path: coverage.cov.html
path: test-coverage.cov.html
7 changes: 4 additions & 3 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Visit https://goreleaser.com for documentation on how to customize this
# behavior.
# Visit https://goreleaser.com for documentation on how to customize this file
version: 2
before:
hooks:
# this is just an example and not a requirement for provider building/publishing
Expand All @@ -14,7 +14,8 @@ builds:
flags:
- -trimpath
ldflags:
- "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}"
- '-s -w -X github.com/microsoft/terraform-provider-power-platform/common.ProviderVersion={{ .Version }} -X github.com/microsoft/terraform-provider-power-platform/common.Commit={{.Commit}}' -X github.com/microsoft/terraform-provider-power-platform/common.Branch=$(git rev-parse --abbrev-ref HEAD)'

goos:
- freebsd
- windows
Expand Down
12 changes: 12 additions & 0 deletions common/release.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package common

var (
// ProviderVersion is the version of the released provider, set during build/release process with ldflags.
// This value can not be const as it is set after the build during the linking process.
ProviderVersion = "0.0.0-dev" // Default value for development builds
Commit = "dev" // Default value for development builds
Branch = "dev" // Default value for development builds
)
4 changes: 2 additions & 2 deletions docs/resources/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ resource "powerplatform_environment" "development" {
cadence = "Moderate"
environment_group_id = ""
dataverse = {
language_code = "1033"
currency_code = "USD"
language_code = "1033"
currency_code = "USD"
domain = "mydomain"
security_group_id = "00000000-0000-0000-0000-000000000000"
}
Expand Down
4 changes: 2 additions & 2 deletions examples/resources/powerplatform_environment/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ resource "powerplatform_environment" "development" {
cadence = "Moderate"
environment_group_id = ""
dataverse = {
language_code = "1033"
currency_code = "USD"
language_code = "1033"
currency_code = "USD"
domain = "mydomain"
security_group_id = "00000000-0000-0000-0000-000000000000"
}
Expand Down
2 changes: 1 addition & 1 deletion internal/powerplatform/api/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (client *ApiClient) ExecuteForGivenScope(ctx context.Context, scope, method
if err != nil {
return nil, err
}
apiResponse, err := client.doRequest(token, request, headers)
apiResponse, err := client.doRequest(ctx, token, request, headers)
if err != nil {
return apiResponse, err
}
Expand Down
44 changes: 42 additions & 2 deletions internal/powerplatform/api/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,49 @@ package api

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"runtime"
"strings"

"github.com/microsoft/terraform-provider-power-platform/common"
"github.com/microsoft/terraform-provider-power-platform/internal/powerplatform/helpers"
)

func (client *ApiClient) doRequest(token *string, request *http.Request, headers http.Header) (*ApiHttpResponse, error) {
func (client *ApiClient) BuildCorrelationHeaders(ctx context.Context) (string, string) {
requestContext, ok := ctx.Value(helpers.REQUEST_CONTEXT_KEY).(helpers.RequestContextValue)
if ok {
cc := strings.Join([]string{
"objectType=" + requestContext.ObjectType,
"objectName=" + requestContext.ObjectName,
"requestType=" + requestContext.RequestType,
}, ",")

rid := "|" + requestContext.RequestId + "." + fmt.Sprintf("%016x", rand.Uint64()) + "."

return rid, cc
}

return "", ""
}

func (client *ApiClient) buildUserAgent(ctx context.Context) string {
userAgent := fmt.Sprintf("terraform-provider-power-platform/%s (%s; %s) terraform/%s go/%s", common.ProviderVersion, runtime.GOOS, runtime.GOARCH, client.Config.TerraformVersion, runtime.Version())

requestContext, ok := ctx.Value(helpers.REQUEST_CONTEXT_KEY).(helpers.RequestContextValue)
if ok {
userAgent += fmt.Sprintf(" %s %s %s", requestContext.ObjectType, requestContext.ObjectName, requestContext.RequestType)
}

return userAgent
}

func (client *ApiClient) doRequest(ctx context.Context, token *string, request *http.Request, headers http.Header) (*ApiHttpResponse, error) {
apiHttpResponse := &ApiHttpResponse{}
if headers != nil {
request.Header = headers
Expand All @@ -33,7 +68,12 @@ func (client *ApiClient) doRequest(token *string, request *http.Request, headers
}

if !client.GetConfig().TelemetryOptout {
request.Header.Set("User-Agent", "terraform-provider-power-platform")
ua := client.buildUserAgent(ctx)
request.Header.Set("User-Agent", ua)

rid, cc := client.BuildCorrelationHeaders(ctx)
request.Header.Set("Request-Id", rid)
request.Header.Set("Correlation-Context", cc)
}

response, err := httpClient.Do(request)
Expand Down
9 changes: 5 additions & 4 deletions internal/powerplatform/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import (
)

type ProviderConfig struct {
Credentials *ProviderCredentials
Urls ProviderConfigUrls
TelemetryOptout bool
Cloud cloud.Configuration
Credentials *ProviderCredentials
Urls ProviderConfigUrls
TelemetryOptout bool
Cloud cloud.Configuration
TerraformVersion string
}

type ProviderConfigUrls struct {
Expand Down
126 changes: 126 additions & 0 deletions internal/powerplatform/helpers/contexts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package helpers

import (
"context"
"fmt"
"strings"

"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/microsoft/terraform-provider-power-platform/common"
)

// ContextKey is a custom type for context keys
// A custom type is needed to avoid collisions with other packages that use the same key
type ContextKey string

type ExecutionContextValue struct {
ProviderVersion string
OperatingSystem string
Architecture string
GoVersion string
}

// RequestContextValue is a struct that holds the object type, request type, object name and request id for a given request
// This struct is used to store the request context in the context so that it can be accessed in lower level functions
type RequestContextValue struct {
ObjectName string
ObjectType string
RequestType string
RequestId string
}

// Context keys for the execution and request context
const (
EXECUTION_CONTEXT_KEY ContextKey = "executionContext"
REQUEST_CONTEXT_KEY ContextKey = "requestContext"
)

// EnterRequestScope is a helper function that logs the start of a request scope and returns a closure that can be used to defer the exit of the request scope
// This function should be called at the start of a resource or data source request function
// The returned closure should be deferred at the start of the function
// The closure will log the end of the request scope
// The context is updated with the request context so that it can be accessed in lower level functions
func EnterRequestContext(ctx context.Context, typ TypeInfo, req any) (context.Context, func()) {
reqId := strings.ReplaceAll(uuid.New().String(), "-", "")
objType, reqType := getRequestType(req)
name:= typ.FullTypeName()

tflog.Debug(ctx, fmt.Sprintf("%s %s START: %s", reqType, objType, name), map[string]any{
"requestId": reqId,
"providerVersion": common.ProviderVersion,
})

// Add the request context to the context so that we can access it in lower level functions
ctx = context.WithValue(ctx, REQUEST_CONTEXT_KEY, RequestContextValue{ObjectType: objType, RequestType: reqType, ObjectName: name, RequestId: reqId})

// This returns a closure that can be used to defer the exit of the request scope
return ctx, func() {
tflog.Debug(ctx, fmt.Sprintf("%s %s END: %s", reqType, objType, name))
}
}

func EnterProviderContext(ctx context.Context, req any) (context.Context, func()) {
objType, reqType := getRequestType(req)

tflog.Debug(ctx, fmt.Sprintf("%s %s START", reqType, objType), map[string]any{
"providerVersion": common.ProviderVersion,
})

// This returns a closure that can be used to defer the exit of the provider scope
return ctx, func() {
tflog.Debug(ctx, fmt.Sprintf("%s %s END", reqType, objType), map[string]any{
"providerVersion": common.ProviderVersion,
})
}
}

// getRequestType returns the object type and request type for a given request
func getRequestType(req any) (string, string) {
switch req.(type) {
case resource.CreateRequest:
return "RESOURCE", "CREATE"
case resource.ReadRequest:
return "RESOURCE", "READ"
case resource.UpdateRequest:
return "RESOURCE", "UPDATE"
case resource.DeleteRequest:
return "RESOURCE", "DELETE"
case resource.SchemaRequest:
return "RESOURCE", "SCHEMA"
case resource.ConfigureRequest:
return "RESOURCE", "CONFIGURE"
case resource.ModifyPlanRequest:
return "RESOURCE", "MODIFY_PLAN"
case resource.ImportStateRequest:
return "RESOURCE", "IMPORT"
case resource.UpgradeStateRequest:
return "RESOURCE", "UPGRADE"
case datasource.ReadRequest:
return "DATA_SOURCE", "READ"
case datasource.SchemaRequest:
return "DATA_SOURCE", "SCHEMA"
case datasource.ConfigureRequest:
return "DATA_SOURCE", "CONFIGURE"
case datasource.MetadataRequest:
return "DATA_SOURCE", "METADATA"
case provider.ConfigureRequest:
return "PROVIDER", "CONFIGURE"
case provider.MetaSchemaRequest:
return "PROVIDER", "METASCHEMA"
case provider.MetadataRequest:
return "PROVIDER", "METADATA"
case provider.SchemaRequest:
return "PROVIDER", "SCHEMA"
case provider.ValidateConfigRequest:
return "PROVIDER", "VALIDATE_CONFIG"
default:
return "UNKNOWN", "UNKNOWN"
}
}
17 changes: 17 additions & 0 deletions internal/powerplatform/helpers/typeinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package helpers

import (
"fmt"
)

type TypeInfo struct {
ProviderTypeName string
TypeName string
}

func (t *TypeInfo) FullTypeName() string {
return fmt.Sprintf("%s_%s", t.ProviderTypeName, t.TypeName)
}
27 changes: 19 additions & 8 deletions internal/powerplatform/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import (
"fmt"
"os"
"regexp"

azcloud "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/microsoft/terraform-provider-power-platform/common"
"github.com/microsoft/terraform-provider-power-platform/internal/powerplatform/api"
"github.com/microsoft/terraform-provider-power-platform/internal/powerplatform/config"
"github.com/microsoft/terraform-provider-power-platform/internal/powerplatform/constants"
Expand Down Expand Up @@ -61,8 +62,11 @@ func NewPowerPlatformProvider(ctx context.Context, testModeEnabled ...bool) func
PowerAppsScope: constants.PUBLIC_POWERAPPS_SCOPE,
PowerPlatformUrl: constants.PUBLIC_POWERPLATFORM_API_DOMAIN,
PowerPlatformScope: constants.PUBLIC_POWERPLATFORM_API_SCOPE,
LicensingUrl: constants.PUBLIC_LICENSING_API_DOMAIN,
},
Cloud: azcloud.AzurePublic,
TerraformVersion: "unknown",
TelemetryOptout: false,
}

if len(testModeEnabled) > 0 && testModeEnabled[0] {
Expand All @@ -81,13 +85,21 @@ func NewPowerPlatformProvider(ctx context.Context, testModeEnabled ...bool) func

func (p *PowerPlatformProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "powerplatform"
resp.Version = common.ProviderVersion

tflog.Debug(ctx, "Provider Metadata request received", map[string]any{
"version": resp.Version,
"typeName": resp.TypeName,
"branch": common.Branch,
"commit": common.Commit,
})
}

func (p *PowerPlatformProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
tflog.Debug(ctx, "Schema request received")
_, exitContext := helpers.EnterProviderContext(ctx, req)
defer exitContext()

resp.Schema = schema.Schema{

Description: "The Power Platform Terraform Provider allows managing environments and other resources within Power Platform",
MarkdownDescription: "The Power Platform Provider allows managing environments and other resources within [Power Platform](https://powerplatform.microsoft.com/)",
Attributes: map[string]schema.Attribute{
Expand Down Expand Up @@ -162,10 +174,10 @@ func (p *PowerPlatformProvider) Schema(ctx context.Context, req provider.SchemaR
}

func (p *PowerPlatformProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config config.ProviderCredentialsModel

tflog.Debug(ctx, "Configure request received")
_, exitContext := helpers.EnterProviderContext(ctx, req)
defer exitContext()

var config config.ProviderCredentialsModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)

if resp.Diagnostics.HasError() {
Expand Down Expand Up @@ -419,15 +431,14 @@ func (p *PowerPlatformProvider) Configure(ctx context.Context, req provider.Conf
}

p.Config.TelemetryOptout = config.TelemetryOptout.ValueBool()
p.Config.TerraformVersion = req.TerraformVersion

providerClient := api.ProviderClient{
Config: p.Config,
Api: p.Api,
}
resp.DataSourceData = &providerClient
resp.ResourceData = &providerClient

tflog.Info(ctx, "Configured API client", map[string]any{"success": true})
}

func (p *PowerPlatformProvider) Resources(ctx context.Context) []func() resource.Resource {
Expand Down
Loading
Loading