Skip to content

Commit e790ba7

Browse files
authored
Merge pull request #325 from hwong0305/logs
Logs - Implementing the ability to Download Logs
2 parents b10a254 + ec2d36b commit e790ba7

File tree

7 files changed

+137
-29
lines changed

7 files changed

+137
-29
lines changed

src/api/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import express from 'express'
22
import adminRouter from './admin'
3+
import logsRouter from './logs'
34
import mappingRouter from './mapping'
45
import sshKeyRouter from './sshKeys'
56
import accessTokensRouter from './accessToken'
@@ -8,6 +9,7 @@ import { getAvailableDomains } from '../lib/data'
89
const apiRouter = express.Router()
910

1011
apiRouter.use('/admin', adminRouter.app)
12+
apiRouter.use('/logs', logsRouter)
1113
apiRouter.use('/mappings', mappingRouter)
1214
apiRouter.use('/sshKeys', sshKeyRouter)
1315
apiRouter.use('/accessTokens', accessTokensRouter)

src/api/logs.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import express from 'express'
2+
import fs from 'fs'
3+
import environment from '../helpers/environment'
4+
import { getMappingByDomain } from '../lib/data'
5+
6+
const logsRouter = express.Router()
7+
const { isProduction } = environment
8+
9+
logsRouter.get('/err/:domain', (req, res) => {
10+
const { domain } = req.params
11+
12+
if (isProduction()) {
13+
// Only search for domain when running in production. The test does not
14+
// require a valid domain since it only verifies the endpoint
15+
const { fullDomain } = getMappingByDomain(domain)
16+
// Pipes the error log files to res
17+
res.setHeader('content-type', 'text/plain')
18+
fs.createReadStream(`/home/myproxy/.pm2/logs/${fullDomain}-err.log`).pipe(
19+
res
20+
)
21+
} else {
22+
res.send('OK')
23+
}
24+
})
25+
26+
logsRouter.get('/out/:domain', (req, res) => {
27+
const { domain } = req.params
28+
29+
if (isProduction()) {
30+
// Only search for domain when running in production. The test does not
31+
// require a valid domain since it only verifies the endpoint
32+
const { fullDomain } = getMappingByDomain(domain)
33+
// Pipes the output log file to res. Console.Log from your app will appear here
34+
res.setHeader('content-type', 'text/plain')
35+
fs.createReadStream(`/home/myproxy/.pm2/logs/${fullDomain}-out.log`).pipe(
36+
res
37+
)
38+
} else {
39+
res.send('OK')
40+
}
41+
})
42+
43+
export default logsRouter

src/api/mapping.ts

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ mappingRouter.post('/', async (req, res) => {
4545
const prodConfigApp = [...prodConfigure.apps][0]
4646
prodConfigApp.name = fullDomain
4747
prodConfigApp.env_production.PORT = parseInt(req.body.port || portCounter, 10)
48-
prodConfigApp.script = 'npm'
49-
prodConfigApp.args = 'run start:myproxy'
48+
prodConfigApp.script = `npm run start:myproxy << /home/myproxy/.pm2/logs/${fullDomain}-out.log`
49+
prodConfigApp.error_file = `/home/myproxy/.pm2/logs/${fullDomain}-err.log`
50+
prodConfigApp.merge_logs = true
5051
const prodConfig = {
5152
apps: prodConfigApp
5253
}
@@ -96,38 +97,34 @@ mappingRouter.post('/', async (req, res) => {
9697

9798
mappingRouter.get('/', async (req, res) => {
9899
const domains = getMappings()
99-
if (isProduction()) {
100-
const data = await exec('su - myproxy -c "pm2 jlist"')
100+
if (!isProduction())
101+
return res.json(domains.map(el => ({ ...el, status: 'not started' })))
102+
const data = await exec('su - myproxy -c "pm2 jlist"')
101103

102-
const outArr = data.stdout.split('\n')
104+
const outArr = data.stdout.split('\n')
103105

104-
const statusData = JSON.parse(outArr[outArr.length - 1]).reduce(
105-
(statusObj, el) => ({
106-
...statusObj,
107-
[el.name]: el.pm2_env.status
108-
}),
109-
{}
110-
)
111-
const fullDomainStatusMapping = domains.map(el => {
112-
if (statusData[el.fullDomain]) {
113-
return { ...el, status: statusData[el.fullDomain] }
114-
} else {
115-
return { ...el, status: 'not started' }
116-
}
117-
})
106+
const statusData = JSON.parse(outArr[outArr.length - 1]).reduce(
107+
(statusObj, el) => ({
108+
...statusObj,
109+
[el.name]: el.pm2_env.status
110+
}),
111+
{}
112+
)
113+
const fullDomainStatusMapping = domains.map(el => {
114+
if (statusData[el.fullDomain]) {
115+
return { ...el, status: statusData[el.fullDomain] }
116+
} else {
117+
return { ...el, status: 'not started' }
118+
}
119+
})
118120

119-
res.json(fullDomainStatusMapping)
120-
} else {
121-
res.json(domains.map(el => ({ ...el, status: 'not started' })))
122-
}
121+
res.json(fullDomainStatusMapping)
123122
})
124123

125124
mappingRouter.delete('/:id', async (req, res) => {
126125
const deletedDomain = getMappingById(req.params.id)
127126
deleteDomain(deletedDomain.fullDomain)
128-
if (!isProduction()) {
129-
return res.json(deletedDomain)
130-
}
127+
if (!isProduction()) return res.json(deletedDomain)
131128
const gitUserId = await getGitUserId()
132129
exec(
133130
`

src/auth/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ const setupAuth = password => {
1414
if (authorization) {
1515
const isCorrect = isCorrectCredentials(authorization as string, password)
1616
if (!adminPass && !isCorrect) res.status(401).send('Unauthorized')
17-
next()
17+
return next()
1818
}
1919
if (!adminPass) return res.render('login', { error: '' })
20-
next()
20+
return next()
2121
}
2222
}
2323

src/public/client.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,21 @@ class MappingItem {
4141
const mappingElement = document.createElement('li')
4242
let iconClass
4343
let iconColor
44+
// LogClass is used to hide the button to download logs when
45+
// pm2 is not managing the apps. Since pm2 is not managing the apps,
46+
// the logs will not be located at the same location
47+
let logClass
4448
if (data.status === 'online') {
4549
iconClass = 'fa fa-circle mr-1 mt-1'
4650
iconColor = 'rgba(50,255,50,0.5)'
51+
logClass = 'fa fa-file-text-o ml-1 mt-1'
4752
} else if (data.status === 'not started') {
4853
iconClass = ''
4954
iconColor = 'transparent'
5055
} else {
5156
iconClass = 'fa fa-circle mr-1 mt-1'
5257
iconColor = 'rgba(255, 50, 50, 0.5)'
58+
logClass = 'fa fa-file-text-o ml-1 mt-1'
5359
}
5460
mappingElement.classList.add(
5561
'list-group-item',
@@ -60,14 +66,23 @@ class MappingItem {
6066
mappingElement.innerHTML = `
6167
<div style='width: 100%'>
6268
<div style='display: flex'>
63-
<i class="${iconClass}" style="font-size: 15px; color: ${iconColor}"></i>
69+
<i class="${iconClass}" style="font-size: 15px; color: ${iconColor}">
70+
</i>
6471
<a class="font-weight-bold"
6572
href="https://${data.fullDomain}">
6673
${data.fullDomain}
6774
</a>
6875
<small class="form-text text-muted ml-1">
6976
PORT: ${data.port}
7077
</small>
78+
<a class="${logClass}"
79+
style="font-size: 15px; color: rgba(255,50,50,0.5)"
80+
href="/api/logs/err/${data.fullDomain}">
81+
</a>
82+
<a class="${logClass}"
83+
style="font-size: 15px; color: rgba(40,167,70,0.5)"
84+
href="/api/logs/out/${data.fullDomain}">
85+
</a>
7186
</div>
7287
<small class="form-text text-muted" style="display: inline-block;">
7388
${data.gitLink}

src/tests/helpers/logAdapter.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const TEST_PORT = process.env.PORT || 50608
2+
const ADMIN = process.env.ADMIN || '123'
3+
const apiURL = `http://127.0.0.1:${TEST_PORT}`
4+
import { Options, Headers } from '../../types/tests'
5+
import fetch, { Response } from 'node-fetch'
6+
7+
const reqHeaders: Headers = {
8+
authorization: ADMIN,
9+
'Content-Type': 'application/json'
10+
}
11+
12+
const logAdapter = (path = '/', method: string): Promise<Response> => {
13+
const options: Options = {
14+
method,
15+
headers: reqHeaders
16+
}
17+
return fetch(`${apiURL}/api/logs${path}`, options)
18+
}
19+
20+
export { logAdapter }

src/tests/integration/log.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { startAppServer } from '../../server/server'
2+
import { logAdapter } from '../helpers/logAdapter'
3+
4+
const TEST_PORT = process.env.PORT || 50608
5+
const ADMIN = process.env.ADMIN || '123'
6+
7+
describe('/api/logs', () => {
8+
let server
9+
10+
beforeAll(async () => {
11+
server = await startAppServer(TEST_PORT, ADMIN)
12+
})
13+
14+
afterAll(() => {
15+
server.close()
16+
})
17+
18+
it('checks that output logs endpoint exists', async () => {
19+
const subDomain = 'Cloud'
20+
const domain = 'Walker'
21+
const logResponse = await logAdapter(`/out/${subDomain}.${domain}`, 'GET')
22+
expect(logResponse.status).toEqual(200)
23+
})
24+
25+
it('checks that error logs endpoint exists', async () => {
26+
const subDomain = 'Luke'
27+
const domain = 'Walker'
28+
const logResponse = await logAdapter(`/err/${subDomain}.${domain}`, 'GET')
29+
expect(logResponse.status).toEqual(200)
30+
})
31+
})

0 commit comments

Comments
 (0)