Skip to content

Commit

Permalink
add telemetry:update (#3050)
Browse files Browse the repository at this point in the history
* Add telemetry:update, change capabilities to signals

* Add test for telemetry:update, fix grpc spelling, display drain after update

* Clarify description of what gets changed

* Add examples, switch signal to signals
  • Loading branch information
eablack authored Oct 22, 2024
1 parent 3f7d253 commit 65c02fb
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 49 deletions.
1 change: 0 additions & 1 deletion cspell-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ Gdvb
getreqs
Ghpcy
githuborg
gprc
hamurai
herokai
herokuapp
Expand Down
24 changes: 6 additions & 18 deletions packages/cli/src/commands/telemetry/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import {Command, flags as Flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import {TelemetryDrain} from '../../lib/types/telemetry'
import heredoc from 'tsheredoc'

import {validateAndFormatSignals} from '../../lib/telemetry/util'
export default class Add extends Command {
static description = 'Add and configure a new telemetry drain. Defaults to collecting all telemetry unless otherwise specified.'

static flags = {
app: Flags.app({exactlyOne: ['app', 'remote', 'space'], description: 'app to add a drain to'}),
remote: Flags.remote({description: 'git remote of app to add a drain to'}),
space: Flags.string({char: 's', description: 'space to add a drain to'}),
signal: Flags.string({default: 'all', description: 'comma-delimited list of signals to collect (traces, metrics, logs). Use "all" to collect all signals.'}),
signals: Flags.string({default: 'all', description: 'comma-delimited list of signals to collect (traces, metrics, logs). Use "all" to collect all signals.'}),
endpoint: Flags.string({required: true, description: 'drain url'}),
transport: Flags.string({required: true, options: ['http', 'gprc'], description: 'transport protocol for the drain'}),
transport: Flags.string({required: true, options: ['http', 'grpc'], description: 'transport protocol for the drain'}),
}

static args = {
Expand All @@ -21,21 +21,9 @@ export default class Add extends Command {

static example = heredoc(`
Add a telemetry drain to an app to collect logs and traces:
$ heroku telemetry:add --signal logs,traces --endpoint https://my-endpoint.com --transport http 'x-drain-example-team: API_KEY x-drain-example-dataset: METRICS_DATASET'
$ heroku telemetry:add --signals logs,traces --endpoint https://my-endpoint.com --transport http 'x-drain-example-team: API_KEY x-drain-example-dataset: METRICS_DATASET'
`)

private validateAndFormatSignal = function (signalInput: string | undefined): string[] {
const signalOptions = ['traces', 'metrics', 'logs']
if (!signalInput || signalInput === 'all') return signalOptions
const signalArray = signalInput.split(',')
signalArray.forEach(signal => {
if (!signalOptions.includes(signal)) {
ux.error(`Invalid signal option: ${signalArray}. Run heroku telemetry:add --help to see signal options.`, {exit: 1})
}
})
return signalArray
}

private getTypeAndName = function (app: string | undefined, space: string | undefined) {
if (app) {
return {type: 'app', name: app}
Expand All @@ -46,15 +34,15 @@ export default class Add extends Command {

public async run(): Promise<void> {
const {flags, args} = await this.parse(Add)
const {app, space, signal, endpoint, transport} = flags
const {app, space, signals, endpoint, transport} = flags
const {headers} = args
const typeAndName = this.getTypeAndName(app, space)
const drainConfig = {
owner: {
type: typeAndName.type,
id: typeAndName.name,
},
signals: this.validateAndFormatSignal(signal),
signals: validateAndFormatSignals(signals),
exporter: {
endpoint,
type: `otlp${transport}`,
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/commands/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export default class Index extends Command {
app: Flags.app({description: 'filter by app name'}),
};

static example = '$ heroku telemetry'

public async run(): Promise<void> {
const {flags} = await this.parse(Index)
const {app, space} = flags
Expand Down Expand Up @@ -37,7 +39,7 @@ export default class Index extends Command {
telemetryDrains,
{
ID: {get: telemetryDrain => telemetryDrain.id},
Signals: {get: telemetryDrain => telemetryDrain.capabilities},
Signals: {get: telemetryDrain => telemetryDrain.signals},
Endpoint: {get: telemetryDrain => telemetryDrain.exporter.endpoint},
[ownerType]: {get: telemetryDrain => telemetryDrain.owner.name},
},
Expand Down
22 changes: 6 additions & 16 deletions packages/cli/src/commands/telemetry/info.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {Command} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import {Args} from '@oclif/core'
import {TelemetryDrain} from '../../lib/types/telemetry'

import {displayTelemetryDrain} from '../../lib/telemetry/util'
export default class Info extends Command {
static topic = 'telemetry'
static description = 'show a telemetry drain\'s info'
static args = {
telemetry_drain_id: Args.string({required: true, description: 'ID of the drain to show info for'}),
};
}

static example = '$ heroku telemetry:info 022e2e2e-2e2e-2e2e-2e2e-2e2e2e2e2e2e'

public async run(): Promise<void> {
const {args} = await this.parse(Info)
Expand All @@ -18,18 +20,6 @@ export default class Info extends Command {
Accept: 'application/vnd.heroku+json; version=3.sdk',
},
})
this.display(telemetryDrain)
}

protected display(telemetryDrain: TelemetryDrain) {
ux.styledHeader(telemetryDrain.id)
const drainType = telemetryDrain.owner.type.charAt(0).toUpperCase() + telemetryDrain.owner.type.slice(1)
ux.styledObject({
[drainType]: telemetryDrain.owner.name,
Signals: telemetryDrain.capabilities.join(', '),
Endpoint: telemetryDrain.exporter.endpoint,
Kind: telemetryDrain.exporter.type,
Headers: telemetryDrain.exporter.headers,
}, ['App', 'Space', 'Signals', 'Endpoint', 'Kind', 'Headers'])
displayTelemetryDrain(telemetryDrain)
}
}
69 changes: 69 additions & 0 deletions packages/cli/src/commands/telemetry/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {flags as Flags, Command} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import {TelemetryDrain, TelemetryDrainWithOptionalKeys, TelemetryExporterWithOptionalKeys} from '../../lib/types/telemetry'
import heredoc from 'tsheredoc'
import {displayTelemetryDrain, validateAndFormatSignals} from '../../lib/telemetry/util'

export default class Update extends Command {
static topic = 'telemetry'
static description = 'updates a telemetry drain with provided attributes (attributes not provided remain unchanged)'
static args = {
telemetry_drain_id: Args.string({required: true, description: 'ID of the drain to update'}),
headers: Args.string({description: 'custom headers to configure the drain in json format'}),
}

static flags = {
signals: Flags.string({description: 'comma-delimited list of signals to collect (traces, metrics, logs). Use "all" to collect all signals.'}),
endpoint: Flags.string({description: 'drain url'}),
transport: Flags.string({options: ['http', 'grpc'], description: 'transport protocol for the drain'}),
}

static example = heredoc(`
$ heroku telemetry:update acde070d-8c4c-4f0d-9d8a-162843c10333 --signals logs,metrics --endpoint https://my-new-endpoint.com
`)

public async run(): Promise<void> {
const {args, flags} = await this.parse(Update)
const {telemetry_drain_id, headers} = args
const {signals, endpoint, transport} = flags
if (!(headers || signals || endpoint || transport)) {
ux.error(heredoc(`
Requires either --signals, --endpoint, --transport or HEADERS to be provided.
See more help with --help
`))
}

const drainConfig: TelemetryDrainWithOptionalKeys = {}
if (signals) {
drainConfig.signals = validateAndFormatSignals(signals)
}

if (headers || endpoint || transport) {
const exporter: TelemetryExporterWithOptionalKeys = {}
if (headers) {
exporter.headers = JSON.parse(headers)
}

if (endpoint) {
exporter.endpoint = endpoint
}

if (transport) {
exporter.type = `otlp${transport}`
}

drainConfig.exporter = exporter
}

ux.action.start(`Updating telemetry drain ${telemetry_drain_id}`)
const {body: telemetryDrain} = await this.heroku.patch<TelemetryDrain>(`/telemetry-drains/${telemetry_drain_id}`, {
headers: {
Accept: 'application/vnd.heroku+json; version=3.sdk',
},
body: drainConfig,
})
ux.action.stop()

displayTelemetryDrain(telemetryDrain)
}
}
26 changes: 26 additions & 0 deletions packages/cli/src/lib/telemetry/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {ux} from '@oclif/core'
import {TelemetryDrain} from '../types/telemetry'

export function validateAndFormatSignals(signalInput: string | undefined): string[] {
const signalOptions = ['traces', 'metrics', 'logs']
if (!signalInput || signalInput === 'all') return signalOptions
const signalArray = signalInput.split(',')
signalArray.forEach(signal => {
if (!signalOptions.includes(signal)) {
ux.error(`Invalid signal option: ${signalArray}. Run heroku telemetry:add --help to see signal options.`, {exit: 1})
}
})
return signalArray
}

export function displayTelemetryDrain(telemetryDrain: TelemetryDrain) {
ux.styledHeader(telemetryDrain.id)
const drainType = telemetryDrain.owner.type.charAt(0).toUpperCase() + telemetryDrain.owner.type.slice(1)
ux.styledObject({
[drainType]: telemetryDrain.owner.name,
Signals: telemetryDrain.signals.join(', '),
Endpoint: telemetryDrain.exporter.endpoint,
Kind: telemetryDrain.exporter.type,
Headers: telemetryDrain.exporter.headers,
}, ['App', 'Space', 'Signals', 'Endpoint', 'Kind', 'Headers'])
}
7 changes: 5 additions & 2 deletions packages/cli/src/lib/types/telemetry.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export type TelemetryDrains = TelemetryDrain[]

export type TelemetryDrain = {
id: string;
capabilities: string[];
signals: string[];
owner: TelemetryDrainOwner;
exporter: TelemetryExporter
}
Expand All @@ -14,7 +14,10 @@ type TelemetryDrainOwner = {
}

type TelemetryExporter = {
type: 'otlphttp' | 'otlpgrpc';
type: string;
endpoint: string;
headers: unknown;
}

type TelemetryDrainWithOptionalKeys = Partial<TelemetryDrain, 'exporter'>
type TelemetryExporterWithOptionalKeys = Partial<TelemetryExporter>
6 changes: 3 additions & 3 deletions packages/cli/test/fixtures/telemetry/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {TelemetryDrain} from '../../../src/lib/types/telemetry'
export const spaceTelemetryDrain1: TelemetryDrain = {
id: '44444321-5717-4562-b3fc-2c963f66afa6',
owner: {id: '12345678-5717-4562-b3fc-2c963f66afa6', type: 'space', name: 'myspace'},
capabilities: ['traces', 'metrics', 'logs'],
signals: ['traces', 'metrics', 'logs'],
exporter: {
type: 'otlphttp',
endpoint: 'https://api.honeycomb.io/',
Expand All @@ -17,7 +17,7 @@ export const spaceTelemetryDrain1: TelemetryDrain = {
export const appTelemetryDrain1: TelemetryDrain = {
id: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
owner: {id: '87654321-5717-4562-b3fc-2c963f66afa6', type: 'app', name: 'myapp'},
capabilities: ['traces', 'metrics'],
signals: ['traces', 'metrics'],
exporter: {
type: 'otlphttp',
endpoint: 'https://api.honeycomb.io/',
Expand All @@ -31,7 +31,7 @@ export const appTelemetryDrain1: TelemetryDrain = {
export const appTelemetryDrain2: TelemetryDrain = {
id: '55555f64-5717-4562-b3fc-2c963f66afa6',
owner: {id: '87654321-5717-4562-b3fc-2c963f66afa6', type: 'app', name: 'myapp'},
capabilities: ['logs'],
signals: ['logs'],
exporter: {
type: 'otlphttp',
endpoint: 'https://api.papertrail.com/',
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/test/unit/commands/telemetry/add.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('telemetry:add', function () {
'{"x-honeycomb-team": "your-api-key", "x-honeycomb-dataset": "your-dataset"}',
'--app',
appId,
'--signal',
'--signals',
'logs',
'--endpoint',
testEndpoint,
Expand All @@ -70,7 +70,7 @@ describe('telemetry:add', function () {
'{"x-honeycomb-team": "your-api-key", "x-honeycomb-dataset": "your-dataset"}',
'--space',
spaceId,
'--signal',
'--signals',
'logs',
'--endpoint',
testEndpoint,
Expand All @@ -87,7 +87,7 @@ describe('telemetry:add', function () {
'{"x-honeycomb-team": "your-api-key", "x-honeycomb-dataset": "your-dataset"}',
'--space',
spaceId,
'--signal',
'--signals',
'logs,foo',
'--endpoint',
testEndpoint,
Expand All @@ -106,7 +106,7 @@ describe('telemetry:add', function () {
'{"x-honeycomb-team": "your-api-key", "x-honeycomb-dataset": "your-dataset"}',
'--space',
spaceId,
'--signal',
'--signals',
'logs,all',
'--endpoint',
testEndpoint,
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/test/unit/commands/telemetry/info.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('telemetry:info', function () {
spaceTelemetryDrain = {
id: '44444321-5717-4562-b3fc-2c963f66afa6',
owner: {id: spaceId, type: 'space', name: 'myspace'},
capabilities: ['traces', 'metrics', 'logs'],
signals: ['traces', 'metrics', 'logs'],
exporter: {
type: 'otlphttp',
endpoint: 'https://api.honeycomb.io/',
Expand All @@ -29,7 +29,7 @@ describe('telemetry:info', function () {
appTelemetryDrain = {
id: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
owner: {id: appId, type: 'app', name: 'myapp'},
capabilities: ['traces', 'metrics'],
signals: ['traces', 'metrics'],
exporter: {
type: 'otlphttp',
endpoint: 'https://api.honeycomb.io/',
Expand All @@ -56,7 +56,7 @@ describe('telemetry:info', function () {
expectOutput(stdout.output, heredoc(`
=== ${spaceTelemetryDrain.id}
Space: ${spaceTelemetryDrain.owner.name}
Signals: ${spaceTelemetryDrain.capabilities.join(', ')}
Signals: ${spaceTelemetryDrain.signals.join(', ')}
Endpoint: ${spaceTelemetryDrain.exporter.endpoint}
Kind: ${spaceTelemetryDrain.exporter.type}
Headers: x-honeycomb-team: 'your-api-key', x-honeycomb-dataset: 'your-dataset'
Expand All @@ -74,7 +74,7 @@ describe('telemetry:info', function () {
expectOutput(stdout.output, heredoc(`
=== ${appTelemetryDrain.id}
App: ${appTelemetryDrain.owner.name}
Signals: ${appTelemetryDrain.capabilities.join(', ')}
Signals: ${appTelemetryDrain.signals.join(', ')}
Endpoint: ${appTelemetryDrain.exporter.endpoint}
Kind: ${appTelemetryDrain.exporter.type}
Headers: x-honeycomb-team: 'your-api-key', x-honeycomb-dataset: 'your-dataset'
Expand Down
Loading

0 comments on commit 65c02fb

Please sign in to comment.