-
Notifications
You must be signed in to change notification settings - Fork 0
Add Database Log table to query database logs for a workspace. #23
Changes from 4 commits
ab6c4ef
23008b3
50d707f
d228be8
87b65fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
sidr0cker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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, | ||
sidr0cker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
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) { | ||
sidr0cker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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) | ||
sidr0cker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else { | ||
err = listOrgWorkspaceDbLogs(ctx, d, h, svc, maxResults, workspace.IdentityId, workspace.Id) | ||
sidr0cker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
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 { | ||
sidr0cker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 { | ||
sidr0cker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @siddharthaturbot Is this change related to the table that this PR is adding? If not, can you please describe why we're making this change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ListActorOrgsResponse is not used by the SDK anymore. Its necessary to add it in the PR or it wont compile. |
||
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 { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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{} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this change related to the table we're adding? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above, Workspace is not part of the struct anymore. |
||
|
||
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) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.