Skip to content

Commit

Permalink
Agent event log
Browse files Browse the repository at this point in the history
  • Loading branch information
clarkmcc committed Nov 20, 2023
1 parent be883a1 commit 2029501
Show file tree
Hide file tree
Showing 24 changed files with 530 additions and 81 deletions.
19 changes: 13 additions & 6 deletions app/backend/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,19 @@ func db(ctx context.Context) database.Database {
return ctx.Value(contextKeyDatabase).(database.Database)
}

type resolveContext struct {
type resolveContext[S any] struct {
context.Context
db database.Database
params graphql.ResolveParams
logger *zap.Logger
source S
}

func (r *resolveContext) getStringArg(name string) string {
func (r *resolveContext[S]) getStringArg(name string) string {
return cast.ToString(r.params.Args[name])
}

func (r *resolveContext) canAccessProject(projectId string) error {
func (r *resolveContext[S]) canAccessProject(projectId string) error {
sub := middleware.SubjectFromContext(r)
if sub == "" {
return fmt.Errorf("no subject in context")
Expand All @@ -53,15 +54,21 @@ func (r *resolveContext) canAccessProject(projectId string) error {
return nil
}

type resolverFunc[T any] func(rctx resolveContext) (T, error)
type resolverFunc[T any, S any] func(rctx resolveContext[S]) (T, error)

func wrapper[T any](fn resolverFunc[T]) func(params graphql.ResolveParams) (any, error) {
func wrapper[S any, T any](fn resolverFunc[T, S]) func(params graphql.ResolveParams) (any, error) {
return func(params graphql.ResolveParams) (any, error) {
ctx := params.Context
return fn(resolveContext{
source, ok := params.Source.(S)
if !ok {
var s S
return nil, fmt.Errorf("invalid source type: %T, expected %T", params.Source, s)
}
return fn(resolveContext[S]{
Context: ctx,
db: db(ctx),
logger: logger(ctx),
source: source,
params: params,
})
}
Expand Down
56 changes: 54 additions & 2 deletions app/backend/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ var hostType = graphql.NewObject(graphql.ObjectConfig{
Type: graphql.NewNonNull(graphql.Boolean),
Resolve: func(p graphql.ResolveParams) (any, error) {
h := p.Source.(types.Host)
if h.LastHeartbeatTimestamp.After(time.Now().Add(-time.Hour)) && h.Online {
// todo: make this configurable
if h.LastHeartbeatTimestamp.After(time.Now().Add(-time.Minute)) && h.Online {
return true, nil
}
return false, nil
Expand Down Expand Up @@ -75,6 +76,13 @@ var hostType = graphql.NewObject(graphql.ObjectConfig{
Type: nullInt64Type,
Description: "The number of CPU cores (i.e. 10).",
},
"events": &graphql.Field{
Type: graphql.NewList(agentEvent),
Description: "The events for the host and its agent.",
Resolve: wrapper[types.Host](func(rctx resolveContext[types.Host]) ([]types.AgentEventLog, error) {
return rctx.db.GetEventLogsByHost(rctx, rctx.source.ID, 15)
}),
},
},
})

Expand All @@ -85,7 +93,7 @@ var hostList = &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: wrapper(func(rctx resolveContext) ([]types.Host, error) {
Resolve: wrapper[any](func(rctx resolveContext[any]) ([]types.Host, error) {
projectID := rctx.getStringArg("projectId")
err := rctx.canAccessProject(projectID)
if err != nil {
Expand All @@ -94,3 +102,47 @@ var hostList = &graphql.Field{
return rctx.db.ListProjectHosts(rctx, projectID)
}),
}

var agentEvent = graphql.NewObject(graphql.ObjectConfig{
Name: "AgentEvent",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
},
"createdAt": &graphql.Field{
Type: graphql.NewNonNull(graphql.DateTime),
},
"agentId": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
},
"hostId": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
},
"type": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
},
"message": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
},
},
})

var hostDetails = &graphql.Field{
Type: hostType,
Args: graphql.FieldConfigArgument{
"hostId": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"projectId": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: wrapper[any](func(rctx resolveContext[any]) (types.Host, error) {
projectID := rctx.getStringArg("projectId")
err := rctx.canAccessProject(projectID)
if err != nil {
return types.Host{}, err
}
return rctx.db.GetHost(rctx, rctx.getStringArg("hostId"), projectID)
}),
}
2 changes: 1 addition & 1 deletion app/backend/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ var projectCreate = &graphql.Field{
Type: graphql.String,
},
},
Resolve: wrapper(func(rctx resolveContext) (map[string]any, error) {
Resolve: wrapper[any](func(rctx resolveContext[any]) (map[string]any, error) {
sub := middleware.SubjectFromContext(rctx)
project, err := rctx.db.CreateProject(rctx, sub,
rctx.getStringArg("name"),
Expand Down
1 change: 1 addition & 0 deletions app/backend/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var schemaConfig = graphql.SchemaConfig{
Name: "Query",
Fields: graphql.Fields{
"hosts": hostList,
"host": hostDetails,
},
}),
Mutation: graphql.NewObject(graphql.ObjectConfig{
Expand Down
2 changes: 1 addition & 1 deletion app/backend/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
var ensureUser = &graphql.Field{
Type: graphql.NewList(projectType),
Args: graphql.FieldConfigArgument{},
Resolve: wrapper(func(rctx resolveContext) ([]types.Project, error) {
Resolve: wrapper[any](func(rctx resolveContext[any]) ([]types.Project, error) {
return rctx.db.UpsertUser(rctx, middleware.SubjectFromContext(rctx))
}),
}
39 changes: 28 additions & 11 deletions app/frontend/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,44 @@ schema {
mutation: Mutation
}

type AgentEvent {
agentId: String!
createdAt: DateTime!
eventType: String!
hostId: String!
id: String!
message: String!
}

type Host {
"The number of CPU cores (i.e. 10)."
cpuCores: Int
cpuCores: NullInt64
"The model of the CPU (i.e. Apple M1 Max)."
cpuModel: String
cpuModel: NullString
createdAt: DateTime!
"The events for the host and its agent."
events: [AgentEvent]
"The hostname of the host."
hostname: String
hostname: NullString
id: String!
"An identifier for the host as determined by the agent. This is usually extracted from the host somehow (i.e. a Host ID)."
identifier: String!
"The architecture of the kernel (i.e. arm64)."
kernelArchitecture: String
kernelArchitecture: NullString
"The version of the kernel (i.e. 23.0.0)."
kernelVersion: String
kernelVersion: NullString
lastHeartbeatTimestamp: DateTime!
online: Boolean!
"The family of the operating system (i.e. Standalone Workstation)."
osFamily: String
osFamily: NullString
"The name of the operating system (i.e. darwin)."
osName: String
osName: NullString
"The version of the operating system (i.e. 14.0)."
osVersion: String
osVersion: NullString
"The private IP address of the host."
privateIpAddress: String
privateIpAddress: NullString
"The public IP address of the host."
publicIpAddress: String
status: Status!
publicIpAddress: NullString
updatedAt: DateTime!
}

Expand All @@ -53,6 +65,7 @@ type ProjectCreate {
}

type Query {
host(hostId: String!, projectId: String!): Host
hosts(projectId: String!): [Host]
}

Expand All @@ -63,3 +76,7 @@ enum Status {

"The `DateTime` scalar type represents a DateTime. The DateTime is serialized as an RFC 3339 quoted string"
scalar DateTime

scalar NullInt64

scalar NullString
55 changes: 55 additions & 0 deletions app/frontend/src/components/host-event-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { AgentEvent } from "@/types";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table.tsx";
import moment from "moment";
import { cn } from "@/lib/utils.ts";
import { AgentEventType } from "@/types/enums.ts";

type HostEventTableProps = {
events: AgentEvent[];
};

export function HostEventTable({ events }: HostEventTableProps) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[250px]">Timestamp</TableHead>
<TableHead className="w-[150px]">Type</TableHead>
<TableHead>Message</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{events.map((event) => (
<TableRow key={event.id}>
<TableCell className="font-medium">
{moment(event.createdAt).format("llll")}
</TableCell>
<TableCell>
<code
className={cn(
"relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold",
AgentEventType.toBackgroundClass(event.type),
)}
>
{event.type}
</code>
</TableCell>
<TableCell>
{event.message}{" "}
<span className="text-gray-500">
{moment(event.createdAt).fromNow()}
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
32 changes: 30 additions & 2 deletions app/frontend/src/components/page-header.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
import { Skeleton } from "@/components/ui/skeleton.tsx";
import { Button } from "@/components/ui/button.tsx";
import { ArrowLeft } from "lucide-react";
import { useNavigate } from "react-router-dom";

type PageHeaderProps = {
title: string;
subtitle: string;
loading?: boolean;
backButton?: boolean;
};

export function PageHeader({ title, subtitle }: PageHeaderProps) {
export function PageHeader({
title,
subtitle,
loading,
backButton,
}: PageHeaderProps) {
const navigate = useNavigate();
if (loading) {
return (
<div className="p-4 pl-7 space-y-3">
<Skeleton className="h-8 w-1/3" />
<Skeleton className="h-6 w-1/4" />
</div>
);
}
return (
<div className="p-4 pl-7">
<h1 className="text-3xl font-bold">{title}</h1>
<div className="flex flex-row items-center space-x-2">
{backButton && (
<Button variant="secondary" size="icon" onClick={() => navigate(-1)}>
<ArrowLeft size={15} />
</Button>
)}
<h1 className="text-3xl font-bold">{title}</h1>
</div>
<span className="text-sm text-gray-400">{subtitle}</span>
</div>
);
Expand Down
Loading

0 comments on commit 2029501

Please sign in to comment.