Skip to content

Commit

Permalink
fix(W-17265927): SSL issues with heroku logs
Browse files Browse the repository at this point in the history
  • Loading branch information
justinwilaby committed Feb 6, 2025
1 parent 081fcd6 commit 9a1e475
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 371 deletions.
1 change: 0 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"@heroku-cli/plugin-ps-exec": "2.6.1",
"@heroku-cli/schema": "^1.0.25",
"@heroku/buildpack-registry": "^1.0.1",
"@heroku/eventsource": "^1.0.7",
"@heroku/heroku-cli-util": "^8.0.13",
"@heroku/http-call": "^5.4.0",
"@inquirer/prompts": "^5.0.5",
Expand Down
104 changes: 67 additions & 37 deletions packages/cli/src/lib/run/log-displayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import colorize from './colorize'
import {LogSession} from '../types/fir'
import {getGenerationByAppId} from '../apps/generation'

const EventSource = require('@heroku/eventsource')

interface LogDisplayerOptions {
app: string,
dyno?: string
Expand All @@ -16,47 +14,79 @@ interface LogDisplayerOptions {
type?: string
}

function readLogs(logplexURL: string, isTail: boolean, recreateSessionTimeout?: number) {
return new Promise<void>(function (resolve, reject) {
const userAgent = process.env.HEROKU_DEBUG_USER_AGENT || 'heroku-run'
const proxy = process.env.https_proxy || process.env.HTTPS_PROXY
const es = new EventSource(logplexURL, {
proxy,
headers: {
'User-Agent': userAgent,
},
})
async function readLogs(logplexURL: string, isTail?:boolean, recreateSessionTimeout?: number) {
// node 20+ will automatically use HTTP_PROXY/HTTPS_PROXY env variables with fetch
const response = await fetch(logplexURL, {
headers: {
'User-Agent': process.env.HEROKU_DEBUG_USER_AGENT || 'heroku-run',
Accept: 'text/event-stream',
},
})

es.addEventListener('error', function (err: { status?: number; message?: string | null }) {
if (err && (err.status || err.message)) {
const msg = (isTail && (err.status === 404 || err.status === 403)) ?
'Log stream timed out. Please try again.' :
`Logs eventsource failed with: ${err.status}${err.message ? ` ${err.message}` : ''}`
reject(new Error(msg))
es.close()
}
if (!response.ok) {
const msg = (isTail && (response.status === 404 || response.status === 403)) ?
'Log stream timed out. Please try again.' :
`Logs stream failed with: ${response.status} ${response.statusText}`
throw new Error(msg)
}

if (!isTail) {
resolve()
es.close()
}
if (!response.body) {
throw new Error('No response body received')
}

// should only land here if --tail and no error status or message
})
const reader = response.body.getReader()

es.addEventListener('message', function (e: { data: string }) {
e.data.trim().split(/\n+/).forEach(line => {
ux.log(colorize(line))
})
})
// logplex sessions seem to timeout after 15 min anyway
let timeoutId: NodeJS.Timeout | undefined
if (isTail && recreateSessionTimeout) {
timeoutId = setTimeout(() => {
reader.cancel()
throw new Error('Fir log stream timeout')
}, recreateSessionTimeout)
}

if (isTail && recreateSessionTimeout) {
setTimeout(() => {
reject(new Error('Fir log stream timeout'))
es.close()
}, recreateSessionTimeout)
try {
await beginReading(reader)
} catch (error) {
reader.cancel()
throw error
} finally {
if (timeoutId) {
clearTimeout(timeoutId)
}
})
}
}

async function beginReading(reader: ReadableStreamDefaultReader<Uint8Array>): Promise<void> {
let buffer = ''
const decoder = new TextDecoder()
while (true) {
const {value, done} = await reader.read()

if (done) {
return
}
// could also use Buffer.from(value).toString() here
// but Buffer has a slight disadvantage in performance

buffer += decoder.decode(value, {stream: true})
const lines = buffer.split('\n')
buffer = lines.pop() || ''

for (const line of lines) {
if (line.trim()) {
// Parse SSE format if the data is in that format

if (line.match(/^id: (.+)$/)) {
continue
}

const match = line.match(/^data: (.+)$/)
const data = match ? match[1] : line
ux.log(colorize(data))
}
}
}
}

async function logDisplayer(heroku: APIClient, options: LogDisplayerOptions) {
Expand Down
Loading

0 comments on commit 9a1e475

Please sign in to comment.