diff --git a/docs/tables/steampipecloud_workspace_db_log.md b/docs/tables/steampipecloud_workspace_db_log.md new file mode 100644 index 0000000..28a342a --- /dev/null +++ b/docs/tables/steampipecloud_workspace_db_log.md @@ -0,0 +1,71 @@ +# Table: steampipecloud_workspace_db_log + +Database logs records the underlying queries executed when a user executes a query. + +## Examples + +### List db logs for an actor by handle + +```sql +select + id, + workspace_id, + workspace_handle, + duration, + query, + log_timestamp +from + steampipecloud_workspace_db_log +where + actor_handle = 'siddharthaturbot'; +``` + +### List db logs for an actor by handle in a particular workspace + +```sql +select + id, + workspace_id, + workspace_handle, + duration, + query, + log_timestamp +from + steampipecloud_workspace_db_log +where + actor_handle = 'siddharthaturbot' + and workspace_handle = 'dev'; +``` + +### List queries that took more than 30 seconds to execute + +```sql +select + id, + workspace_id, + workspace_handle, + duration, + query, + log_timestamp +from + steampipecloud_workspace_db_log +where + duration > 30000; +``` + +### List all queries that ran in my workspace in the last hour + +```sql +select + id, + workspace_id, + workspace_handle, + duration, + query, + log_timestamp +from + steampipecloud_workspace_db_log +where + workspace_handle = 'dev' + and log_timestamp > now() - interval '1 hr'; +``` diff --git a/go.mod b/go.mod index 17c4233..d9b2ae7 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module steampipe-plugin-steampipecloud go 1.18 require ( - github.com/turbot/steampipe-cloud-sdk-go v0.1.2 + github.com/turbot/steampipe-cloud-sdk-go v0.1.3 github.com/turbot/steampipe-plugin-sdk/v3 v3.3.1 ) diff --git a/go.sum b/go.sum index 5a521b8..97090b2 100644 --- a/go.sum +++ b/go.sum @@ -241,10 +241,10 @@ github.com/tkrajina/go-reflector v0.5.4 h1:dS9aJEa/eYNQU/fwsb5CSiATOxcNyA/gG/A7a github.com/tkrajina/go-reflector v0.5.4/go.mod h1:9PyLgEOzc78ey/JmQQHbW8cQJ1oucLlNQsg8yFvkVk8= github.com/turbot/go-kit v0.4.0 h1:EdD7Bf2EGAjvHRGQxRiWpDawzZSk3T+eghqbj74qiSc= github.com/turbot/go-kit v0.4.0/go.mod h1:SBdPRngbEfYubiR81iAVtO43oPkg1+ASr+XxvgbH7/k= +github.com/turbot/steampipe-cloud-sdk-go v0.1.3 h1:j4D2S9XCATa0o1wxLuKr/F/uJM8shZer0/zvTfWOgvE= +github.com/turbot/steampipe-cloud-sdk-go v0.1.3/go.mod h1:8M2CspUHgCGqDCJV+FNn+boBPyLRHyzDinYnoZ/kZYw= github.com/turbot/steampipe-plugin-sdk/v3 v3.3.1 h1:y6ExizWQkkllN5t4S3nFFaZuyttIW8agg/CNuhCjUoE= github.com/turbot/steampipe-plugin-sdk/v3 v3.3.1/go.mod h1:8r7CDDlrSUd5iUgPlvPa5ttlZ4OEFNMHm8fdwgnn5WM= -github.com/turbot/steampipe-cloud-sdk-go v0.1.2 h1:H0F3zld6kX4uO2B5igV/r0GSOgqm3Zu6VEBHcwXvt1Y= -github.com/turbot/steampipe-cloud-sdk-go v0.1.2/go.mod h1:8M2CspUHgCGqDCJV+FNn+boBPyLRHyzDinYnoZ/kZYw= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= diff --git a/steampipecloud/plugin.go b/steampipecloud/plugin.go index 77f1b5e..60eb760 100644 --- a/steampipecloud/plugin.go +++ b/steampipecloud/plugin.go @@ -30,6 +30,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "steampipecloud_workspace_connection": tableSteampipeCloudWorkspaceConnection(ctx), "steampipecloud_workspace_mod": tableSteampipeCloudWorkspaceMod(ctx), "steampipecloud_workspace_mod_variable": tableSteampipeCloudWorkspaceModVariable(ctx), + "steampipecloud_workspace_db_log": tableSteampipeCloudWorkspaceDBLog(ctx), }, } diff --git a/steampipecloud/table_steampipecloud_db_log.go b/steampipecloud/table_steampipecloud_db_log.go new file mode 100644 index 0000000..37ea59f --- /dev/null +++ b/steampipecloud/table_steampipecloud_db_log.go @@ -0,0 +1,242 @@ +package steampipecloud + +import ( + "context" + + openapi "github.com/turbot/steampipe-cloud-sdk-go" + + "github.com/turbot/steampipe-plugin-sdk/v3/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/v3/plugin" + "github.com/turbot/steampipe-plugin-sdk/v3/plugin/transform" +) + +//// TABLE DEFINITION + +func tableSteampipeCloudWorkspaceDBLog(_ context.Context) *plugin.Table { + return &plugin.Table{ + Name: "steampipecloud_workspace_db_log", + Description: "Database logs records the underlying queries executed when a user executes a query.", + List: &plugin.ListConfig{ + ParentHydrate: listWorkspaces, + Hydrate: listWorkspaceDBLogs, + }, + Columns: []*plugin.Column{ + { + Name: "id", + Description: "The unique identifier for a db log.", + Type: proto.ColumnType_STRING, + Transform: transform.FromCamel(), + }, + { + Name: "actor_id", + Description: "The unique identifier for the user who executed the query.", + Type: proto.ColumnType_STRING, + Transform: transform.FromCamel(), + }, + { + Name: "actor_handle", + Description: "The handle of the user who executed the query.", + Type: proto.ColumnType_STRING, + }, + { + Name: "actor_display_name", + Description: "The display name of the user who executed the query.", + Type: proto.ColumnType_STRING, + }, + { + Name: "actor_avatar_url", + Description: "The avatar of the user who executed the query.", + Type: proto.ColumnType_STRING, + Transform: transform.FromCamel(), + }, + { + Name: "workspace_id", + Description: "The unique identifier of the workspace on which the query was executed.", + Type: proto.ColumnType_STRING, + Transform: transform.FromCamel(), + }, + { + Name: "workspace_handle", + Description: "The handle of the workspace on which the query was executed.", + Type: proto.ColumnType_STRING, + }, + { + Name: "duration", + Description: "The duration of the query in milliseconds(ms).", + Type: proto.ColumnType_DOUBLE, + Transform: transform.FromCamel(), + }, + { + Name: "query", + Description: "The query that was executed in the workspace.", + Type: proto.ColumnType_STRING, + }, + { + Name: "log_timestamp", + Description: "The time when the log got captured in postgres.", + Type: proto.ColumnType_TIMESTAMP, + }, + { + Name: "created_at", + Description: "The time when the db log record was generated.", + Type: proto.ColumnType_STRING, + }, + }, + } +} + +//// LIST FUNCTION + +func listWorkspaceDBLogs(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + // Get the workspace object from the parent hydrate + workspace := h.Item.(*openapi.Workspace) + + // Create the connection + svc, err := connect(ctx, d) + if err != nil { + plugin.Logger(ctx).Error("listDBLogs", "connection_error", err) + return nil, err + } + + // Get cached user identity + getUserIdentityCached := plugin.HydrateFunc(getUserIdentity).WithCache() + commonData, err := getUserIdentityCached(ctx, d, h) + if err != nil { + plugin.Logger(ctx).Error("listDBLogs", "getUserIdentityCached", err) + return nil, err + } + + // Extract the user object from the cached identity + user := commonData.(openapi.User) + + // If the requested number of items is less than the paging max limit + // set the limit to that instead + maxResults := int32(100) + limit := d.QueryContext.Limit + if d.QueryContext.Limit != nil { + if *limit < int64(maxResults) { + if *limit < 1 { + maxResults = int32(1) + } else { + maxResults = int32(*limit) + } + } + } + + // If we want to get the db logs for the user + if user.Id == workspace.IdentityId { + err = listUserWorkspaceDBLogs(ctx, d, h, svc, maxResults, user.Id, workspace.Id) + } else { + err = listOrgWorkspaceDBLogs(ctx, d, h, svc, maxResults, workspace.IdentityId, workspace.Id) + } + if err != nil { + plugin.Logger(ctx).Error("listDBLogs", "error", err) + return nil, err + } + + if err != nil { + plugin.Logger(ctx).Error("listDBLogs", "error", err) + return nil, err + } + return nil, nil +} + +func listUserWorkspaceDBLogs(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData, svc *openapi.APIClient, maxResults int32, identityId, workspaceId string) error { + var err error + + // execute list call + pagesLeft := true + var resp openapi.ListLogsResponse + var listDetails func(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) + + for pagesLeft { + if resp.NextToken != nil { + listDetails = func(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + resp, _, err = svc.UserWorkspaces.ListDBLogs(ctx, identityId, workspaceId).NextToken(*resp.NextToken).Limit(maxResults).Execute() + return resp, err + } + } else { + listDetails = func(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + resp, _, err = svc.UserWorkspaces.ListDBLogs(ctx, identityId, workspaceId).Limit(maxResults).Execute() + return resp, err + } + } + + response, err := plugin.RetryHydrate(ctx, d, h, listDetails, &plugin.RetryConfig{ShouldRetryError: shouldRetryError}) + + if err != nil { + plugin.Logger(ctx).Error("listUserDBLogs", "list", err) + return err + } + + result := response.(openapi.ListLogsResponse) + + if result.HasItems() { + for _, log := range *result.Items { + d.StreamListItem(ctx, log) + + // Context can be cancelled due to manual cancellation or the limit has been hit + if d.QueryStatus.RowsRemaining(ctx) == 0 { + return nil + } + } + } + if resp.NextToken == nil { + pagesLeft = false + } else { + resp.NextToken = result.NextToken + } + } + + return nil +} + +func listOrgWorkspaceDBLogs(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData, svc *openapi.APIClient, maxResults int32, identityId, workspaceId string) error { + var err error + + // execute list call + pagesLeft := true + var resp openapi.ListLogsResponse + var listDetails func(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) + + for pagesLeft { + if resp.NextToken != nil { + listDetails = func(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + resp, _, err = svc.OrgWorkspaces.ListDBLogs(ctx, identityId, workspaceId).NextToken(*resp.NextToken).Limit(maxResults).Execute() + return resp, err + } + } else { + listDetails = func(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + resp, _, err = svc.OrgWorkspaces.ListDBLogs(ctx, identityId, workspaceId).Limit(maxResults).Execute() + return resp, err + } + } + + response, err := plugin.RetryHydrate(ctx, d, h, listDetails, &plugin.RetryConfig{ShouldRetryError: shouldRetryError}) + + if err != nil { + plugin.Logger(ctx).Error("listOrgDBLogs", "list", err) + return err + } + + result := response.(openapi.ListLogsResponse) + + if result.HasItems() { + for _, log := range *result.Items { + d.StreamListItem(ctx, log) + + // Context can be cancelled due to manual cancellation or the limit has been hit + if d.QueryStatus.RowsRemaining(ctx) == 0 { + return nil + } + } + } + if result.NextToken == nil { + pagesLeft = false + } else { + resp.NextToken = result.NextToken + } + } + + return nil +} diff --git a/steampipecloud/table_steampipecloud_organization.go b/steampipecloud/table_steampipecloud_organization.go index 9cc1b18..537a786 100644 --- a/steampipecloud/table_steampipecloud_organization.go +++ b/steampipecloud/table_steampipecloud_organization.go @@ -110,7 +110,7 @@ func listOrganizations(ctx context.Context, d *plugin.QueryData, h *plugin.Hydra // execute list call pagesLeft := true - var resp openapi.ListActorOrgsResponse + var resp openapi.ListUserOrgsResponse var listDetails func(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) for pagesLeft { @@ -133,7 +133,7 @@ func listOrganizations(ctx context.Context, d *plugin.QueryData, h *plugin.Hydra return nil, err } - result := response.(openapi.ListActorOrgsResponse) + result := response.(openapi.ListUserOrgsResponse) if result.HasItems() { for _, org := range *result.Items { diff --git a/steampipecloud/table_steampipecloud_workspace_mod.go b/steampipecloud/table_steampipecloud_workspace_mod.go index 7f7a165..05743ed 100644 --- a/steampipecloud/table_steampipecloud_workspace_mod.go +++ b/steampipecloud/table_steampipecloud_workspace_mod.go @@ -179,8 +179,6 @@ func listUserWorkspaceMods(ctx context.Context, d *plugin.QueryData, h *plugin.H if result.HasItems() { for _, workspaceMod := range *result.Items { - workspaceMod.Workspace = &openapi.Workspace{} - workspaceMod.Workspace.Handle = workspaceHandle d.StreamListItem(ctx, workspaceMod) // Context can be cancelled due to manual cancellation or the limit has been hit @@ -231,8 +229,6 @@ func listOrgWorkspaceMods(ctx context.Context, d *plugin.QueryData, h *plugin.Hy if result.HasItems() { for _, workspaceMod := range *result.Items { - workspaceMod.Workspace = &openapi.Workspace{} - workspaceMod.Workspace.Handle = workspaceHandle d.StreamListItem(ctx, workspaceMod) // Context can be cancelled due to manual cancellation or the limit has been hit @@ -312,7 +308,6 @@ func getUserWorkspaceMod(ctx context.Context, d *plugin.QueryData, h *plugin.Hyd response, err := plugin.RetryHydrate(ctx, d, h, getDetails, &plugin.RetryConfig{ShouldRetryError: shouldRetryError}) workspaceMod := response.(openapi.WorkspaceMod) - workspaceMod.Workspace = &openapi.Workspace{} if err != nil { plugin.Logger(ctx).Error("getUserWorkspaceMod", "get", err) @@ -336,7 +331,6 @@ func getOrgWorkspaceMod(ctx context.Context, d *plugin.QueryData, h *plugin.Hydr response, err := plugin.RetryHydrate(ctx, d, h, getDetails, &plugin.RetryConfig{ShouldRetryError: shouldRetryError}) workspaceMod := response.(openapi.WorkspaceMod) - workspaceMod.Workspace = &openapi.Workspace{} if err != nil { plugin.Logger(ctx).Error("getOrgWorkspaceMod", "get", err)