Skip to content

Commit

Permalink
chore(cli): Add OpenTelemetry (#8653)
Browse files Browse the repository at this point in the history
* Initial OTel kernel for CLI

* Add helper functions for telemetry

* Add telemetry for info command

* Capture and record top level errors

* WIP compute of telemetry details in the background

* wip changes to the background compute

* Instrument CLI commands

I'm not in love with this code. Seems difficult to ensure key consisitency but I'm working around the existing code and yargs.

* Rework OTel exporting.

Switched to use a custom exporter which writes spans to a file. Then have a background job that fires when OTel shuts down to read those saved spans and send them to the collector.
This means we have one background job that runs to both compute the telemetry resources and send the data to the collector.

* Re-enable existing telemetry and remove error messaging

* Revert change to locking.js

* Refactor the new telemetry setup

Move some files around and rename some. Adds in the missing telemetry fields to the resource. Rewrites the spans to disk with the added resource information for verbose visibility.

* Remove stray console logs and remove events from spans.

* Produce top level error output and record an error ref. code

We will likely have to iterate on how the output looks.

* fix experiments and webBundler resource values

* Tidy telemetry/index.js

* Record top level errors with legacy telemetry also

* Use error exit code if it exists

I wouldn't have written this myself but I seen it used in the code so I may as well keep using it.

* Started to remove the handler level try/catch.

Stopped because of some many edge or special cases where the CLI immediately exists with non-zero exit code.

* Remove unused imports

* Add back try/catch for the lint command

* Update test-fixture

* Revert and just hardcode the command name

* Revert change to vite.config.ts

* Add missing otel api dep

* Make use of fs-extra json funcs

* Replace require with import

* Simplify the exitCode setting

* Fix telemetry yargs option

* Introduce background job helper function

The helper ensure's we maintain the spawn options we need to support different platforms. It also ensures the output is written to a log file within the '.redwood' folder.

Reworks the new telemetry background process to use this helper. Introduces some relatively verbose output to the send process now that we do not need to be quiet.

* lint

* refactor telemetry enabling

---------

Co-authored-by: Dominic Saadi <dominiceliassaadi@gmail.com>
  • Loading branch information
Josh-Walker-GM and jtoar committed Jun 21, 2023
1 parent 0e23c6e commit a87e8a4
Show file tree
Hide file tree
Showing 77 changed files with 1,195 additions and 139 deletions.
1 change: 1 addition & 0 deletions packages/cli-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"dependencies": {
"@babel/core": "7.22.5",
"@babel/runtime-corejs3": "7.22.5",
"@opentelemetry/api": "1.4.1",
"@redwoodjs/project-config": "5.0.0",
"@redwoodjs/telemetry": "5.0.0",
"chalk": "4.1.2",
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-helpers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export * from './lib/project'
export * from './auth/setupHelpers'

export * from './lib/installHelpers'

export * from './telemetry/index'
46 changes: 46 additions & 0 deletions packages/cli-helpers/src/telemetry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import opentelemetry, {
SpanStatusCode,
AttributeValue,
Span,
} from '@opentelemetry/api'

type TelemetryAttributes = {
[key: string]: AttributeValue
}

/**
* Safely records attributes to the opentelemetry span
*
* @param attributes An object of key-value pairs to be individually recorded as attributes
* @param span An optional span to record the attributes to. If not provided, the current active span will be used
*/
export function recordTelemetryAttributes(
attributes: TelemetryAttributes,
span?: Span
) {
const spanToRecord = span ?? opentelemetry.trace.getActiveSpan()
if (spanToRecord === undefined) {
return
}
for (const [key, value] of Object.entries(attributes)) {
spanToRecord.setAttribute(key, value)
}
}

/**
* Safely records an error to the opentelemetry span
*
* @param error An error to record to the span
* @param span An optional span to record the error to. If not provided, the current active span will be used
*/
export function recordTelemetryError(error: any, span?: Span) {
const spanToRecord = span ?? opentelemetry.trace.getActiveSpan()
if (spanToRecord === undefined) {
return
}
spanToRecord.setStatus({
code: SpanStatusCode.ERROR,
message: error.toString().split('\n')[0],
})
spanToRecord.recordException(error)
}
7 changes: 7 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
"dependencies": {
"@babel/runtime-corejs3": "7.22.5",
"@iarna/toml": "2.2.5",
"@opentelemetry/api": "1.4.1",
"@opentelemetry/exporter-trace-otlp-http": "0.40.0",
"@opentelemetry/resources": "1.14.0",
"@opentelemetry/sdk-trace-node": "1.14.0",
"@opentelemetry/semantic-conventions": "1.14.0",
"@prisma/internals": "4.15.0",
"@redwoodjs/api-server": "5.0.0",
"@redwoodjs/cli-helpers": "5.0.0",
Expand All @@ -43,6 +48,7 @@
"boxen": "5.1.2",
"camelcase": "6.3.0",
"chalk": "4.1.2",
"ci-info": "3.8.0",
"concurrently": "8.2.0",
"configstore": "3.1.5",
"core-js": "3.31.0",
Expand Down Expand Up @@ -70,6 +76,7 @@
"secure-random-password": "0.2.3",
"semver": "7.5.2",
"string-env-interpolation": "1.0.1",
"systeminformation": "5.18.3",
"terminal-link": "2.1.1",
"title-case": "3.0.3",
"uuid": "9.0.0",
Expand Down
33 changes: 18 additions & 15 deletions packages/cli/src/commands/buildHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { Listr } from 'listr2'
import { rimraf } from 'rimraf'
import terminalLink from 'terminal-link'

import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'
import { buildApi } from '@redwoodjs/internal/dist/build/api'
import { loadAndValidateSdls } from '@redwoodjs/internal/dist/validateSchema'
import { detectPrerenderRoutes } from '@redwoodjs/prerender/detection'
import { timedTelemetry, errorTelemetry } from '@redwoodjs/telemetry'
import { timedTelemetry } from '@redwoodjs/telemetry'

import { getPaths, getConfig } from '../lib'
import c from '../lib/colors'
import { generatePrismaCommand } from '../lib/generatePrismaClient'

export const handler = async ({
Expand All @@ -23,6 +23,15 @@ export const handler = async ({
prisma = true,
prerender,
}) => {
recordTelemetryAttributes({
command: 'build',
side: JSON.stringify(side),
verbose,
performance,
stats,
prisma,
prerender,
})
const rwjsPaths = getPaths()

if (performance) {
Expand Down Expand Up @@ -152,18 +161,12 @@ export const handler = async ({
renderer: verbose && 'verbose',
})

try {
await timedTelemetry(process.argv, { type: 'build' }, async () => {
await jobs.run()
await timedTelemetry(process.argv, { type: 'build' }, async () => {
await jobs.run()

if (side.includes('web') && prerender) {
// This step is outside Listr so that it prints clearer, complete messages
await triggerPrerender()
}
})
} catch (e) {
console.log(c.error(e.message))
errorTelemetry(process.argv, e.message)
process.exit(1)
}
if (side.includes('web') && prerender) {
// This step is outside Listr so that it prints clearer, complete messages
await triggerPrerender()
}
})
}
5 changes: 5 additions & 0 deletions packages/cli/src/commands/check.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'

import { getPaths } from '../lib'
import c from '../lib/colors'

Expand All @@ -7,6 +9,9 @@ export const description =
'Get structural diagnostics for a Redwood project (experimental)'

export const handler = () => {
recordTelemetryAttributes({
command,
})
// Deep dive
//
// It seems like we have to use `require` here instead of `await import`
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/commands/consoleHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'fs'
import path from 'path'
import repl from 'repl'

import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'
import { registerApiSideBabelHook } from '@redwoodjs/internal/dist/build/babel/api'

import { getPaths } from '../lib'
Expand Down Expand Up @@ -35,6 +36,10 @@ const loadConsoleHistory = async (r) => {
}

export const handler = () => {
recordTelemetryAttributes({
command: 'console',
})

// Transpile on the fly
registerApiSideBabelHook({
plugins: [
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/commands/dataMigrate/install.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import terminalLink from 'terminal-link'

import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'

export const command = 'install'
export const description = 'Add the RW_DataMigration model to your schema'

Expand All @@ -13,6 +15,9 @@ export function builder(yargs) {
}

export async function handler(options) {
recordTelemetryAttributes({
command: ['data-migrate', command].join(' '),
})
const { handler } = await import('./installHandler.js')
return handler(options)
}
6 changes: 6 additions & 0 deletions packages/cli/src/commands/dataMigrate/up.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import terminalLink from 'terminal-link'

import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'

import { getPaths } from '../../lib'

export const command = 'up'
Expand Down Expand Up @@ -32,6 +34,10 @@ export function builder(yargs) {
}

export async function handler(options) {
recordTelemetryAttributes({
command: ['data-migrate', command].join(' '),
dbFromDist: options.importDbClientFromDist,
})
const { handler } = await import('./upHandler.js')
return handler(options)
}
16 changes: 16 additions & 0 deletions packages/cli/src/commands/deploy/baremetal.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { env as envInterpolation } from 'string-env-interpolation'
import terminalLink from 'terminal-link'
import { titleCase } from 'title-case'

import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'

import { getPaths } from '../../lib'
import c from '../../lib/colors'

Expand Down Expand Up @@ -684,6 +686,20 @@ export const commands = (yargs, ssh) => {
}

export const handler = async (yargs) => {
recordTelemetryAttributes({
command: ['deploy', 'baremetal'].join(' '),
firstRun: yargs.firstRun,
update: yargs.update,
install: yargs.install,
migrate: yargs.migrate,
build: yargs.build,
restart: yargs.restart,
cleanup: yargs.cleanup,
maintenance: yargs.maintenance,
rollback: yargs.rollback,
verbose: yargs.verbose,
})

const { NodeSSH } = require('node-ssh')
const ssh = new NodeSSH()

Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/commands/deploy/edgio.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import fs from 'fs-extra'
import { omit } from 'lodash'
import terminalLink from 'terminal-link'

import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'
import { getPaths } from '@redwoodjs/project-config'

import c from '../../lib/colors'
Expand Down Expand Up @@ -45,6 +46,14 @@ const execaOptions = {
}

export const handler = async (args) => {
recordTelemetryAttributes({
command: ['deploy', 'edgio'].join(' '),
skipInit: args.skipInit,
build: args.build,
prisma: args.prisma,
dataMigrate: args.dataMigrate,
})

const { builder: edgioBuilder } = require('@edgio/cli/commands/deploy')
const cwd = path.join(getPaths().base)

Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/commands/deploy/flightcontrol.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'path'
import execa from 'execa'
import terminalLink from 'terminal-link'

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

import { getPaths } from '../../lib'
Expand Down Expand Up @@ -44,6 +45,13 @@ export const builder = (yargs) => {
}

export const handler = async ({ side, serve, prisma, dm: dataMigrate }) => {
recordTelemetryAttributes({
command: ['deploy', 'flightcontrol'].join(' '),
side,
prisma,
dataMigrate,
serve,
})
const rwjsPaths = getPaths()

const execaConfig = {
Expand Down
9 changes: 8 additions & 1 deletion packages/cli/src/commands/deploy/netlify.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'

import { deployBuilder, deployHandler } from './helpers/helpers'

export const command = 'netlify [...commands]'
export const description = 'Build command for Netlify deploy'

export const builder = (yargs) => deployBuilder(yargs)

export const handler = deployHandler
export const handler = (yargs) => {
recordTelemetryAttributes({
command: ['deploy', 'netlify'].join(' '),
})
deployHandler(yargs)
}
8 changes: 8 additions & 0 deletions packages/cli/src/commands/deploy/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'path'
import execa from 'execa'
import terminalLink from 'terminal-link'

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

import { getPaths } from '../../lib'
Expand Down Expand Up @@ -44,6 +45,13 @@ if (process.argv.slice(2).includes('api')) {
}

export const handler = async ({ side, prisma, dm: dataMigrate }) => {
recordTelemetryAttributes({
command: ['deploy', 'render'].join(' '),
side,
prisma,
dataMigrate,
})

const rwjsPaths = getPaths()

const execaConfig = {
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/commands/deploy/serverless.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Listr } from 'listr2'
import prompts from 'prompts'
import terminalLink from 'terminal-link'

import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'

import { getPaths } from '../../lib'
import c from '../../lib/colors'

Expand Down Expand Up @@ -127,6 +129,14 @@ const loadDotEnvForStage = (dotEnvPath) => {
}

export const handler = async (yargs) => {
recordTelemetryAttributes({
command: ['deploy', 'serverless'].join(' '),
sides: JSON.stringify(yargs.sides),
verbose: yargs.verbose,
packOnly: yargs.packOnly,
firstRun: yargs.firstRun,
})

const rwjsPaths = getPaths()
const dotEnvPath = path.join(rwjsPaths.base, `.env.${yargs.stage}`)

Expand Down
12 changes: 11 additions & 1 deletion packages/cli/src/commands/deploy/vercel.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'

import { deployBuilder, deployHandler } from './helpers/helpers'

export const command = 'vercel [...commands]'
export const description = 'Build command for Vercel deploy'

export const builder = (yargs) => deployBuilder(yargs)

export const handler = deployHandler
export const handler = (yargs) => {
recordTelemetryAttributes({
command: ['deploy', 'vercel'].join(' '),
build: yargs.build,
prisma: yargs.prisma,
dataMigrate: yargs.dataMigrate,
})
deployHandler(yargs)
}
5 changes: 5 additions & 0 deletions packages/cli/src/commands/destroy/graphiql/graphiql.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Listr } from 'listr2'

import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'

import {
existsAnyExtensionSync,
deleteFile,
Expand Down Expand Up @@ -33,6 +35,9 @@ export const command = 'graphiql'
export const description = 'Destroy graphiql header'

export const handler = () => {
recordTelemetryAttributes({
command: ['destory', 'graphiql'].join(' '),
})
const path = getOutputPath()
const tasks = new Listr(
[
Expand Down
Loading

0 comments on commit a87e8a4

Please sign in to comment.