Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add telemetry to the proxy #26695

Merged
merged 48 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
0da3f04
chore: set up instrumentation and instrument middleware
AtofStryker May 1, 2023
d99cdd4
chore: set up console exporter
AtofStryker May 2, 2023
54aaa3b
chore: add parent span option to telemetry package
AtofStryker May 5, 2023
01c1efc
chore: set up telemetry verbose mode
AtofStryker May 5, 2023
cd79147
chore: instrument the network proxy - part 1
mjhenkes May 9, 2023
693e8fe
chore: make sure to terminate spans when request is aborted
AtofStryker May 5, 2023
958067d
fix telemetry, create/end the request middle prior to sending the out…
brian-mann May 4, 2023
3d87b88
avoid telemetry ts build step, create entrypoint into packages/teleme…
brian-mann May 4, 2023
b1932c0
allow env vars to be "true" or "1"
brian-mann May 4, 2023
96c1d21
when creating child span, inherit their attributes directly from the …
brian-mann May 4, 2023
3f1561f
create custom honeycomb exporter and span processor to log traces
brian-mann May 5, 2023
5fac6fc
remove duplicate code that's already called in this.setRootContext
brian-mann May 5, 2023
63bc55c
cleanup
mjhenkes May 9, 2023
69f528d
more clean up
mjhenkes May 9, 2023
b5ae111
update honeycomb network:proxy attributes, update console.log message
brian-mann May 9, 2023
4b9e414
yarn lock
mjhenkes May 9, 2023
473cb73
chore: remove performance API in middleware
AtofStryker May 9, 2023
79e6c27
chore: end response on correct event
AtofStryker May 10, 2023
da073fd
recursively gather parent attributes on close
mjhenkes May 10, 2023
2c16cae
added key and some clean up
mjhenkes May 11, 2023
c70fbbd
github action detector, move verbose into index, verbose log commands
mjhenkes May 11, 2023
e6d543f
some tests
mjhenkes May 11, 2023
c7b68ba
clean up honeycomb exporter
mjhenkes May 12, 2023
101370e
some renaming
mjhenkes May 12, 2023
fab0bde
testing console trace link exporter
mjhenkes May 12, 2023
8fd414b
Don't lose the top span when running in verbose.
mjhenkes May 12, 2023
3522cbe
link to the right place for prod/dev
mjhenkes May 12, 2023
f69d6b9
changes to verbose to make sure it is read in the browser
mjhenkes May 12, 2023
c25ebf8
Merge branch 'develop' into matth/chore/proxy-telemetry
mjhenkes May 15, 2023
6becdce
Apply suggestions from code review
mjhenkes May 15, 2023
59025ae
pass parent attributes between telemetry instances
mjhenkes May 15, 2023
afdbae1
default to false
mjhenkes May 15, 2023
1be8cd1
'fix' build issues
mjhenkes May 15, 2023
4a3d207
src not dist
mjhenkes May 15, 2023
4574358
add back on start span
mjhenkes May 15, 2023
98b8c23
Merge branch 'develop' into matth/chore/proxy-telemetry
mjhenkes May 15, 2023
f7b860b
once more with feeling
mjhenkes May 15, 2023
b7b9e83
Fix some tests
mjhenkes May 15, 2023
bc31ab5
Merge branch 'develop' into matth/chore/proxy-telemetry
mjhenkes May 15, 2023
f3a0bf4
try this i guess
mjhenkes May 15, 2023
7b16e05
revert auto build
mjhenkes May 15, 2023
4f1835b
Apply suggestions from code review
mjhenkes May 16, 2023
3b533ac
support failed commands
mjhenkes May 16, 2023
deb28f2
Address PR comments
mjhenkes May 16, 2023
21e5594
Address PR Comments
mjhenkes May 16, 2023
bb0fbdd
Merge branch 'develop' into matth/chore/proxy-telemetry
mjhenkes May 17, 2023
3ca6bba
error handling
mjhenkes May 17, 2023
ac6bf6a
handle all the errors
mjhenkes May 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions packages/app/src/runner/events/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,69 @@ export const addTelemetryListeners = (Cypress) => {
// TODO: log error when client side debug logging is available
}
})

const commandSpanInfo = (command: Cypress.CommandQueue) => {
const runnable = Cypress.state('runnable')
const runnableType = runnable.type === 'hook' ? runnable.hookName : runnable.type

return {
name: `${runnableType}: ${command.attributes.name}(${command.attributes.args.join(',')})`,
runnable,
runnableType,
}
}

Cypress.on('command:start', (command: Cypress.CommandQueue) => {
try {
const test = Cypress.state('test')

const { name, runnable, runnableType } = commandSpanInfo(command)

const span = telemetry.startSpan({
name,
opts: {
attributes: {
spec: runnable.invocationDetails.relativeFile,
test: `test:${test.fullTitle()}`,
'runnable-type': runnableType,
},
},
isVerbose: true,
})

span?.setAttribute('command-name', command.attributes.name)
} catch (error) {
// TODO: log error when client side debug logging is available
}
})

const onCommandEnd = (command: Cypress.CommandQueue) => {
try {
const span = telemetry.getSpan(commandSpanInfo(command).name)

span?.setAttribute('state', command.state)
span?.setAttribute('numLogs', command.logs?.length || 0)
span?.end()
} catch (error) {
// TODO: log error when client side debug logging is available
}
}

Cypress.on('command:end', onCommandEnd)

Cypress.on('skipped:command:end', onCommandEnd)

Cypress.on('command:failed', (command: Cypress.CommandQueue, error: Error) => {
mjhenkes marked this conversation as resolved.
Show resolved Hide resolved
try {
const span = telemetry.getSpan(commandSpanInfo(command).name)

span?.setAttribute('state', command.state)
span?.setAttribute('numLogs', command.logs?.length || 0)
span?.setAttribute('error.name', error.name)
span?.setAttribute('error.message', error.message)
span?.end()
} catch (error) {
// TODO: log error when client side debug logging is available
}
})
}
2 changes: 1 addition & 1 deletion packages/data-context/src/sources/HtmlDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export class HtmlDataSource {
window.__CYPRESS_CONFIG__ = ${JSON.stringify(serveConfig)};
window.__CYPRESS_TESTING_TYPE__ = '${this.ctx.coreData.currentTestingType}'
window.__CYPRESS_BROWSER__ = ${JSON.stringify(this.ctx.coreData.activeBrowser)}
${telemetry.isEnabled() ? `window.__CYPRESS_TELEMETRY__ = ${JSON.stringify({ context: telemetry.getActiveContextObject(), resources: telemetry.getResources() })}` : ''}
${telemetry.isEnabled() ? `window.__CYPRESS_TELEMETRY__ = ${JSON.stringify({ context: telemetry.getActiveContextObject(), resources: telemetry.getResources(), isVerbose: telemetry.isVerbose() })}` : ''}
${process.env.CYPRESS_INTERNAL_GQL_NO_SOCKET ? `window.__CYPRESS_GQL_NO_SOCKET__ = 'true';` : ''}
</script>
`)
Expand Down
3 changes: 3 additions & 0 deletions packages/driver/src/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,9 @@ class $Cypress {
case 'cy:command:end':
return this.emit('command:end', ...args)

case 'cy:command:failed':
return this.emit('command:failed', ...args)

case 'cy:skipped:command:end':
return this.emit('skipped:command:end', ...args)
mjhenkes marked this conversation as resolved.
Show resolved Hide resolved

Expand Down
1 change: 1 addition & 0 deletions packages/driver/src/cypress/command_queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,7 @@ export class CommandQueue extends Queue<$Command> {
current.fail()
}

Cypress.action('cy:command:failed', current, err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double checking - Should this be command:fails OR command:skipped dep on the command state of queued vs pending?

this would be primarily observers around cy.session spans and error handling if I remember corretly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't expect skipped to count as an error. There is an explicit skipped:command:end message called. Even so, the span would be ended with the state logged, but it would also include the error.

this.cleanup()

return this.cy.fail(err)
Expand Down
12 changes: 12 additions & 0 deletions packages/net-stubbing/lib/server/middleware/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ import {
getBodyEncoding,
} from '../util'
import { InterceptedRequest } from '../intercepted-request'
import { telemetry } from '@packages/telemetry'

// do not use a debug namespace in this file - use the per-request `this.debug` instead
// available as cypress-verbose:proxy:http
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const debug = null

export const SetMatchingRoutes: RequestMiddleware = async function () {
const span = telemetry.startSpan({ name: 'set:matching:routes', parentSpan: this.reqMiddlewareSpan, isVerbose: true })

if (matchesRoutePreflight(this.netStubbingState.routes, this.req)) {
// send positive CORS preflight response
return sendStaticResponse(this, {
Expand All @@ -41,15 +44,20 @@ export const SetMatchingRoutes: RequestMiddleware = async function () {

this.req.matchingRoutes = [...getRoutesForRequest(this.netStubbingState.routes, this.req)]

span?.end()
this.next()
}

/**
* Called when a new request is received in the proxy layer.
*/
export const InterceptRequest: RequestMiddleware = async function () {
const span = telemetry.startSpan({ name: 'intercept:request', parentSpan: this.reqMiddlewareSpan, isVerbose: true })

if (!this.req.matchingRoutes?.length) {
// not intercepted, carry on normally...
span?.end()

return this.next()
}

Expand Down Expand Up @@ -166,8 +174,12 @@ export const InterceptRequest: RequestMiddleware = async function () {
if (request.responseSent) {
// request has been fulfilled with a response already, do not send the request outgoing
// @see https://github.com/cypress-io/cypress/issues/15841
span?.end()

return this.end()
}

span?.end()

return request.continueRequest()
}
1 change: 0 additions & 1 deletion packages/net-stubbing/lib/server/middleware/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { concatStream, httpUtils } from '@packages/network'
import Debug from 'debug'
import type { Readable } from 'stream'
import { getEncoding } from 'istextorbinary'

import type {
ResponseMiddleware,
} from '@packages/proxy'
Expand Down
53 changes: 41 additions & 12 deletions packages/proxy/lib/http/index.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,41 @@
import Bluebird from 'bluebird'
import chalk from 'chalk'
import Debug from 'debug'
import _ from 'lodash'
import { errorUtils } from '@packages/errors'
import { DeferredSourceMapCache } from '@packages/rewriter'
import { telemetry, Span } from '@packages/telemetry'
import ErrorMiddleware from './error-middleware'
import RequestMiddleware from './request-middleware'
import ResponseMiddleware from './response-middleware'
import { HttpBuffers } from './util/buffers'
import { GetPreRequestCb, PreRequests } from './util/prerequests'

import type EventEmitter from 'events'
import type CyServer from '@packages/server'
import type {
CypressIncomingRequest,
CypressOutgoingResponse,
BrowserPreRequest,
} from '@packages/proxy'
import Debug from 'debug'
import chalk from 'chalk'
import ErrorMiddleware from './error-middleware'
import { HttpBuffers } from './util/buffers'
import { GetPreRequestCb, PreRequests } from './util/prerequests'
import type { IncomingMessage } from 'http'
import type { NetStubbingState } from '@packages/net-stubbing'
import Bluebird from 'bluebird'
import type { Readable } from 'stream'
import type { Request, Response } from 'express'
import RequestMiddleware from './request-middleware'
import ResponseMiddleware from './response-middleware'
import { DeferredSourceMapCache } from '@packages/rewriter'
import type { RemoteStates } from '@packages/server/lib/remote_states'
import type { CookieJar, SerializableAutomationCookie } from '@packages/server/lib/util/cookies'
import type { RequestedWithAndCredentialManager } from '@packages/server/lib/util/requestedWithAndCredentialManager'
import { errorUtils } from '@packages/errors'

function getRandomColorFn () {
return chalk.hex(`#${Number(
Math.floor(Math.random() * 0xFFFFFF),
).toString(16).padStart(6, 'F').toUpperCase()}`)
}

export const isVerboseTelemetry = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this exported and why is this hard-coded to true?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a single place to control the verbose telemetry flag for the proxy, the request and response middle-wares both import it.


const isVerbose = isVerboseTelemetry

export const debugVerbose = Debug('cypress-verbose:proxy:http')

export enum HttpStages {
Expand All @@ -49,6 +55,9 @@ export type HttpMiddlewareStacks = {
type HttpMiddlewareCtx<T> = {
req: CypressIncomingRequest
res: CypressOutgoingResponse
handleHttpRequestSpan?: Span
reqMiddlewareSpan?: Span
resMiddlewareSpan?: Span
shouldCorrelatePreRequests: () => boolean
stage: HttpStages
debug: Debug.Debugger
Expand Down Expand Up @@ -164,6 +173,7 @@ export function _runStage (type: HttpStages, ctx: any, onError: Function) {

function _end (retval?) {
ctx.res.off('close', onClose)

if (ended) {
return
}
Expand Down Expand Up @@ -215,6 +225,7 @@ export function _runStage (type: HttpStages, ctx: any, onError: Function) {
middleware.call(fullCtx)
} catch (err) {
err.message = `Internal error while proxying "${ctx.req.method} ${ctx.req.proxiedUrl}" in ${middlewareName}:\n${err.message}`

errorUtils.logError(err)
fullCtx.onError(err)
}
Expand Down Expand Up @@ -255,7 +266,6 @@ export class Http {
constructor (opts: ServerCtx & { middleware?: HttpMiddlewareStacks }) {
this.buffers = new HttpBuffers()
this.deferredSourceMapCache = new DeferredSourceMapCache(opts.request)

this.config = opts.config
this.shouldCorrelatePreRequests = opts.shouldCorrelatePreRequests || (() => false)
this.getFileServerToken = opts.getFileServerToken
Expand All @@ -273,7 +283,7 @@ export class Http {
}
}

handle (req: CypressIncomingRequest, res: CypressOutgoingResponse) {
handleHttpRequest (req: CypressIncomingRequest, res: CypressOutgoingResponse, handleHttpRequestSpan?: Span) {
const colorFn = debugVerbose.enabled ? getRandomColorFn() : undefined
const debugUrl = debugVerbose.enabled ?
(req.proxiedUrl.length > 80 ? `${req.proxiedUrl.slice(0, 80)}...` : req.proxiedUrl)
Expand All @@ -282,6 +292,7 @@ export class Http {
const ctx: HttpMiddlewareCtx<any> = {
req,
res,
handleHttpRequestSpan,
buffers: this.buffers,
config: this.config,
shouldCorrelatePreRequests: this.shouldCorrelatePreRequests,
Expand Down Expand Up @@ -331,6 +342,14 @@ export class Http {
return _runStage(HttpStages.Error, ctx, onError)
}

// start the span that is responsible for recording the start time of the entire middleware run on the stack
// make this span a part of the middleware ctx so we can keep names simple when correlating
ctx.reqMiddlewareSpan = telemetry.startSpan({
name: 'request:middleware',
parentSpan: handleHttpRequestSpan,
isVerbose,
})

return _runStage(HttpStages.IncomingRequest, ctx, onError)
.then(() => {
// If the response has been destroyed after handling the incoming request, it implies the that request was canceled by the browser.
Expand All @@ -340,7 +359,17 @@ export class Http {
}

if (ctx.incomingRes) {
// start the span that is responsible for recording the start time of the entire middleware run on the stack
ctx.resMiddlewareSpan = telemetry.startSpan({
name: 'response:middleware',
parentSpan: handleHttpRequestSpan,
isVerbose,
})

return _runStage(HttpStages.IncomingResponse, ctx, onError)
.finally(() => {
ctx.resMiddlewareSpan?.end()
})
}

return ctx.debug('Warning: Request was not fulfilled with a response.')
Expand Down
Loading