Skip to content

Commit

Permalink
feat(studio): v2 studio (#8173)
Browse files Browse the repository at this point in the history
* WIP updates

* WIP studio updates

* WIP: Studio v2

* Add map view breadcrump and map landing page

* Initial support for search/filtering - limit and sort

* add basic span filtering on some fields

* add redwood-function to frontend feature list

* update frontend-dist

* Additional briefs, minor UI change

* Rebuild frontend

* fix handler options

---------

Co-authored-by: David Thyresson <dthyresson@gmail.com>
  • Loading branch information
Josh-Walker-GM and dthyresson authored May 10, 2023
1 parent fd341a0 commit d38ace4
Show file tree
Hide file tree
Showing 102 changed files with 5,532 additions and 2,403 deletions.
17 changes: 11 additions & 6 deletions packages/cli/src/commands/experimental/studio.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ export const description = 'Run the Redwood development studio'
export const EXPERIMENTAL_TOPIC_ID = 4771

export function builder(yargs) {
yargs.epilogue(
`Also see the ${terminalLink(
'Redwood CLI Reference',
'https://redwoodjs.com/docs/cli-commands#studio'
)}`
)
yargs
.option('open', {
default: true,
description: 'Open the studio in your browser',
})
.epilogue(
`Also see the ${terminalLink(
'Redwood CLI Reference',
'https://redwoodjs.com/docs/cli-commands#studio'
)}`
)
}

export async function handler(options) {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/experimental/studioHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { isModuleInstalled, installRedwoodModule } from '../../lib/packages'
import { command, description, EXPERIMENTAL_TOPIC_ID } from './studio'
import { printTaskEpilogue } from './util'

export const handler = async () => {
export const handler = async (options) => {
printTaskEpilogue(command, description, EXPERIMENTAL_TOPIC_ID)
try {
// Check the module is installed
Expand Down Expand Up @@ -41,7 +41,7 @@ export const handler = async () => {

// Import studio and start it
const { start } = await import('@redwoodjs/studio')
await start()
await start({ open: options.open })
} catch (e) {
console.log('Cannot start the development studio')
console.log(e)
Expand Down
71 changes: 0 additions & 71 deletions packages/studio/backend/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,74 +22,3 @@ export const getDatabase = async () => {
}
return db
}

export const setupTables = async () => {
const db = await getDatabase()

// BIGINT for UnixNano times will break in 239 years (Fri Apr 11 2262 23:47:16 GMT+0000)
const spanTableSQL = `CREATE TABLE IF NOT EXISTS span (id TEXT PRIMARY KEY, trace TEXT NOT NULL, parent TEXT, name TEXT, kind INTEGER, status_code INTEGER, status_message TEXT, start_nano BIGINT, end_nano BIGINT, duration_nano BIGINT, events JSON, attributes JSON, resources, JSON);`
await db.exec(spanTableSQL)
}

export const setupViews = async () => {
const prismaQueriesView = `
CREATE VIEW IF NOT EXISTS prisma_queries as SELECT DISTINCT
s.id,
s.trace,
s.parent as parent_id,
p.trace as parent_trace,
s.name,
json_extract(p. "attributes", '$.method') AS method,
json_extract(p. "attributes", '$.model') AS model,
json_extract(p. "attributes", '$.name') AS prisma_name,
s.start_nano,
s.end_nano,
s.duration_nano,
cast((s.duration_nano / 1000000.000) as REAL) as duration_ms,
cast((s.duration_nano / 1000000000.0000) as number) as duration_sec,
json_extract(s. "attributes", '$."db.statement"') AS db_statement
FROM
span s
JOIN span p ON s.trace = p.trace
WHERE
s. "name" = 'prisma:engine:db_query'
AND
p. "name" = 'prisma:client:operation'
ORDER BY s.start_nano desc, s.parent;
`
await db.exec(prismaQueriesView)

const SQLSpansView = `
CREATE VIEW IF NOT EXISTS sql_spans AS
SELECT DISTINCT
*,
cast((duration_nano / 1000000.000) as REAL) as duration_ms,
cast((duration_nano / 1000000000.0000) as number) as duration_sec
FROM
span
WHERE
json_extract(attributes, '$."db.statement"') IS NOT NULL
ORDER BY start_nano desc;
`
await db.exec(SQLSpansView)

const graphQLSpansView = `CREATE VIEW IF NOT EXISTS graphql_spans AS
SELECT
id,
parent,
name,
json_extract(ATTRIBUTES, '$."graphql.resolver.fieldName"') AS field_name,
json_extract(ATTRIBUTES, '$."graphql.resolver.typeName"') AS type_name,
start_nano,
end_nano,
duration_nano
FROM
span
WHERE
field_name IS NOT NULL
OR type_name IS NOT NULL
ORDER BY
start_nano DESC;`

await db.exec(graphQLSpansView)
}
16 changes: 10 additions & 6 deletions packages/studio/backend/fastify/spanIngester.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FastifyInstance } from 'fastify'

import { getDatabase } from '../database'
import { retypeSpan } from '../services/span'
import type {
RawAttribute,
RestructuredAttributes,
Expand Down Expand Up @@ -61,6 +62,11 @@ export default async function routes(fastify: FastifyInstance, _options: any) {
fastify.post('/v1/traces', async (request, _reply) => {
const data: { resourceSpans: ResourceSpan[] } = request.body as any

const db = await getDatabase()
const spanInsertStatement = await db.prepare(
'INSERT INTO span (id, trace, parent, name, kind, status_code, status_message, start_nano, end_nano, duration_nano, events, attributes, resources) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, json(?), json(?), json(?)) RETURNING id;'
)

// TODO: Consider better typing here`
const spans: RestructuredSpan[] = []

Expand Down Expand Up @@ -103,13 +109,8 @@ export default async function routes(fastify: FastifyInstance, _options: any) {
}

for (const span of spans) {
const db = await getDatabase()

// Insert the span
const spanInsertStatement = await db.prepare(
'INSERT INTO span (id, trace, parent, name, kind, status_code, status_message, start_nano, end_nano, duration_nano, events, attributes, resources) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, json(?), json(?), json(?)) RETURNING *;'
)
const spanInsertResult = await spanInsertStatement.run(
const spanInsertResult = await spanInsertStatement.get(
span.id,
span.trace,
span.parent,
Expand All @@ -124,6 +125,9 @@ export default async function routes(fastify: FastifyInstance, _options: any) {
JSON.stringify(span.attributes),
JSON.stringify(span.resourceAttributes)
)
if (spanInsertResult.id) {
await retypeSpan(undefined, { id: spanInsertResult.id })
}
return spanInsertResult
}

Expand Down
110 changes: 86 additions & 24 deletions packages/studio/backend/graphql/yoga.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
import type { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
import { JSONDefinition, JSONResolver } from 'graphql-scalars'
import { createYoga, createSchema } from 'graphql-yoga'

import { authProvider, generateAuthHeaders } from '../services/auth'
import { spanTypeTimeline, spanTreeMapData } from '../services/charts'
import { studioConfig, webConfig } from '../services/config'
import { graphQLSpans, graphQLSpanCount } from '../services/graphqlSpans'
import { span, spans } from '../services/explore/span'
import { traces, trace, traceCount } from '../services/explore/trace'
import { prismaQuerySpans } from '../services/prismaSpans'
import { traces, trace, traceCount } from '../services/span'
import { sqlSpans, sqlCount } from '../services/sqlSpans'
import { retypeSpans, truncateSpans } from '../services/span'
import { getAncestorSpans, getDescendantSpans } from '../services/util'

export const setupYoga = (fastify: FastifyInstance) => {
const schema = createSchema<{
req: FastifyRequest
reply: FastifyReply
}>({
typeDefs: /* GraphQL */ `
${JSONDefinition}
# HTTP
type HttpSpan {
id: String!
span: Span
}
# GraphQL
type GraphQLSpan {
id: String!
span: Span
}
# Traces
type Trace {
id: String
spans: [Span]
enhancements: TraceEnhancements
}
type TraceEnhancements {
features: [String]
containsError: Boolean
}
# Spans
type Span {
# From OTEL
id: String
trace: String
parent: String
Expand All @@ -34,9 +50,24 @@ export const setupYoga = (fastify: FastifyInstance) => {
startNano: String
endNano: String
durationNano: String
events: String # JSON
attributes: String # JSON
resources: String # JSON
events: [JSON]
attributes: JSON
resources: JSON
# Enrichments
type: String
brief: String
descendantSpans: [Span]
ancestorSpans: [Span]
}
type SpanTypeTimelineData {
data: [JSON]
keys: [String!]
index: String
legend: JSON
axisLeft: JSON
axisBottom: JSON
}
type PrismaQuerySpan {
Expand Down Expand Up @@ -96,34 +127,65 @@ export const setupYoga = (fastify: FastifyInstance) => {
}
type Query {
traces: [Trace]!
trace(id: String!): Trace
prismaQueries(id: String!): [PrismaQuerySpan]!
authProvider: String
studioConfig: StudioConfig
webConfig: WebConfig
generateAuthHeaders(userId: String): AuthHeaders
sqlSpans: [Span]!
sqlCount: Int!
graphQLSpans: [GraphQLSpan]!
graphQLSpanCount: Int!
traceCount: Int!
# Explore - Tracing
traceCount: Int
trace(traceId: String): Trace
traces(searchFilter: String): [Trace]
# Explore - Span
span(spanId: String!): Span
spans(searchFilter: String): [Span]
# Charts
spanTypeTimeline(
timeLimit: Int!
timeBucket: Int!
): SpanTypeTimelineData
spanTreeMapData(spanId: String): JSON
}
type Mutation {
retypeSpans: Boolean!
truncateSpans: Boolean!
}
`,
resolvers: {
JSON: JSONResolver,
Mutation: {
retypeSpans,
truncateSpans,
},
Query: {
traces,
trace,
studioConfig,
webConfig,
authProvider,
generateAuthHeaders,
prismaQueries: prismaQuerySpans,
sqlSpans,
sqlCount,
// Explore - Tracing
traceCount,
graphQLSpans,
graphQLSpanCount,
trace,
traces,
// Explore - Span
span,
spans,
// Charts
spanTypeTimeline,
spanTreeMapData,
},
Span: {
descendantSpans: async (span, _args, _ctx) => {
return getDescendantSpans(span.id)
},
ancestorSpans: async (span, _args, _ctx) => {
return getAncestorSpans(span.id)
},
},
},
})
Expand Down
14 changes: 9 additions & 5 deletions packages/studio/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@ import Fastify from 'fastify'
import type { FastifyInstance } from 'fastify'
import open from 'open'

import { setupTables, setupViews } from './database'
import withApiProxy from './fastify/plugins/withApiProxy'
import reactRoutes from './fastify/react'
import spanRoutes from './fastify/spanIngester'
import yogaRoutes from './fastify/yoga'
import { setupYoga } from './graphql/yoga'
import { getWebConfig } from './lib/config'
import { runMigrations } from './migrations'

const HOST = 'localhost'
const PORT = 4318

let fastify: FastifyInstance

export const start = async () => {
export const start = async (
{ open: autoOpen }: { open: boolean } = { open: false }
) => {
process.on('SIGTERM', async () => {
await stop()
})
Expand All @@ -27,8 +29,7 @@ export const start = async () => {
})

// DB Setup
await setupTables()
await setupViews()
await runMigrations()

// Fasitfy Setup
fastify = Fastify({
Expand Down Expand Up @@ -64,7 +65,10 @@ export const start = async () => {
fastify.listen({ port: PORT, host: HOST })
fastify.ready(() => {
console.log(`Studio API listening on ${HOST}:${PORT}`)
open(`http://${HOST}:${PORT}`)

if (autoOpen) {
open(`http://${HOST}:${PORT}`)
}
})
}

Expand Down
4 changes: 2 additions & 2 deletions packages/studio/backend/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ApiConfig, StudioConfig, WebConfig } from 'backend/types'

import { getConfig } from '@redwoodjs/project-config'

import type { ApiConfig, StudioConfig, WebConfig } from '../types'

export const getApiConfig = (): ApiConfig => {
return getConfig().api
}
Expand Down
Loading

0 comments on commit d38ace4

Please sign in to comment.