• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

screwdriver-cd / screwdriver / #3090

04 Apr 2025 09:37PM UTC coverage: 95.199% (-0.06%) from 95.262%
#3090

Pull #3317

screwdriver

web-flow
Merge branch 'master' into preResponse
Pull Request #3317: feat: log request payload when available

1976 of 2133 branches covered (92.64%)

Branch coverage included in aggregate %.

15 of 16 new or added lines in 1 file covered. (93.75%)

4945 of 5137 relevant lines covered (96.26%)

103.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.2
/lib/server.js
1
'use strict';
2

3
const Hapi = require('@hapi/hapi');
6✔
4
const logger = require('screwdriver-logger');
6✔
5
const registerPlugins = require('./registerPlugins');
6✔
6

7
process.on('unhandledRejection', (reason, p) => {
6✔
8
    console.error('Unhandled Rejection at: Promise', p, 'reason:', reason); /* eslint-disable-line no-console */
12✔
9
});
10

11
/**
12
 * @method handlePreResponseLogs
13
 * @param  {Hapi.Request}    request Hapi Request object
14
 * @param  {Hapi.h}     h   Hapi Response Toolkit
15
 */
16
function handlePreResponseLogs(request, h) {
17
    const { response } = request;
6✔
18
    const { release } = request.server.app;
6✔
19

20
    if (release && release.cookieName && request.state && !request.state[release.cookieName]) {
6✔
21
        h.state(release.cookieName, release.cookieValue);
5✔
22
    }
23

24
    // Pretty print errors
25
    if (response.isBoom) {
6✔
26
        const err = response;
4✔
27
        const errName = err.output.payload.error;
4✔
28
        const errMessage = err.message;
4✔
29
        const { statusCode } = err.output.payload;
4✔
30
        const stack = err.stack || errMessage;
4!
31

32
        // If we're throwing errors, let's have them say a little more than just 500
33
        if (statusCode === 500) {
4✔
34
            request.log(['server', 'error'], stack);
2✔
35
        }
36

37
        const res = {
4✔
38
            statusCode,
39
            error: errName,
40
            message: errMessage
41
        };
42

43
        if (err.data) {
4✔
44
            res.data = err.data;
1✔
45
        }
46

47
        return h.response(res).code(statusCode);
4✔
48
    }
49

50
    // Log request payload when it takes longer than 5 seconds to respond
51
    // This is to prevent logging payloads for every request
52
    if (request.info && request.info.received && Date.now() - request.info.received > 5000 && request.payload) {
2!
NEW
53
        request.log(['payload'], {
×
54
            method: request.method,
55
            path: request.path,
56
            payload: request.payload,
57
            statusCode: request.response && request.response.statusCode,
×
58
            responseTime: Date.now() - request.info.received
59
        });
60
    }
61

62
    return h.continue;
2✔
63
}
64

65
/**
66
 * Configures & starts up a HapiJS server
67
 * @method
68
 * @param  {Object}      config
69
 * @param  {Object}      config.httpd
70
 * @param  {Integer}     config.httpd.port          Port number to listen to
71
 * @param  {String}      config.httpd.host          Host to listen on
72
 * @param  {String}      config.httpd.uri           Public routable address
73
 * @param  {Object}      config.httpd.tls           TLS Configuration
74
 * @param  {Object}      config.webhooks            Webhooks settings
75
 * @param  {String}      config.webhooks.restrictPR Restrict PR setting
76
 * @param  {Boolean}     config.webhooks.chainPR    Chain PR flag
77
 * @param  {Object}      config.ecosystem           List of hosts in the ecosystem
78
 * @param  {Object}      config.ecosystem.ui        URL for the User Interface
79
 * @param  {Factory}     config.pipelineFactory     Pipeline Factory instance
80
 * @param  {Factory}     config.jobFactory          Job Factory instance
81
 * @param  {Factory}     config.userFactory         User Factory instance
82
 * @param  {Factory}     config.bannerFactory       Banner Factory instance
83
 * @param  {Factory}     config.buildFactory        Build Factory instance
84
 * @param  {Factory}     config.buildClusterFactory Build Cluster Factory instance
85
 * @param  {Factory}     config.stepFactory         Step Factory instance
86
 * @param  {Factory}     config.secretFactory       Secret Factory instance
87
 * @param  {Factory}     config.tokenFactory        Token Factory instance
88
 * @param  {Factory}     config.eventFactory        Event Factory instance
89
 * @param  {Factory}     config.collectionFactory   Collection Factory instance
90
 * @param  {Factory}     config.stageFactory        Stage Factory instance
91
 * @param  {Factory}     config.stageBuildFactory   Stage Build Factory instance
92
 * @param  {Factory}     config.triggerFactory      Trigger Factory instance
93
 * @param  {Object}      config.builds              Config to include for builds plugin
94
 * @param  {Object}      config.builds.ecosystem    List of hosts in the ecosystem
95
 * @param  {Object}      config.builds.authConfig   Configuration for auth
96
 * @param  {Object}      config.builds.externalJoin Flag to allow external join
97
 * @param  {Object}      config.unzipArtifactsEnabled  Flag to allow unzip artifacts
98
 * @param  {Object}      config.artifactsMaxDownloadSize Maximum download size for artifacts
99
 * @param  {Function}    callback                   Callback to invoke when server has started.
100
 * @return {http.Server}                            A listener: NodeJS http.Server object
101
 */
102

103
module.exports = async config => {
6✔
104
    try {
6✔
105
        // Hapi Cross-origin resource sharing configuration
106
        // See http://hapijs.com/api for available options
107

108
        let corsOrigins = [config.ecosystem.ui];
6✔
109

110
        if (Array.isArray(config.ecosystem.allowCors)) {
6!
111
            corsOrigins = corsOrigins.concat(config.ecosystem.allowCors);
6✔
112
        }
113

114
        const cors = {
6✔
115
            origin: corsOrigins,
116
            additionalExposedHeaders: ['x-more-data'],
117
            credentials: true
118
        };
119
        // Create a server with a host and port
120
        const server = new Hapi.Server({
6✔
121
            port: config.httpd.port,
122
            host: config.httpd.host,
123
            uri: config.httpd.uri,
124
            routes: {
125
                cors,
126
                log: { collect: true }
127
            },
128
            state: {
129
                strictHeader: false
130
            },
131
            router: {
132
                stripTrailingSlash: true
133
            }
134
        });
135

136
        // Set the factories within server.app
137
        // Instantiating the server with the factories will apply a shallow copy
138
        server.app = {
6✔
139
            commandFactory: config.commandFactory,
140
            commandTagFactory: config.commandTagFactory,
141
            templateFactory: config.templateFactory,
142
            templateTagFactory: config.templateTagFactory,
143
            pipelineTemplateFactory: config.pipelineTemplateFactory,
144
            pipelineTemplateVersionFactory: config.pipelineTemplateVersionFactory,
145
            jobTemplateTagFactory: config.jobTemplateTagFactory,
146
            pipelineTemplateTagFactory: config.pipelineTemplateTagFactory,
147
            stageFactory: config.stageFactory,
148
            stageBuildFactory: config.stageBuildFactory,
149
            triggerFactory: config.triggerFactory,
150
            pipelineFactory: config.pipelineFactory,
151
            jobFactory: config.jobFactory,
152
            userFactory: config.userFactory,
153
            buildFactory: config.buildFactory,
154
            stepFactory: config.stepFactory,
155
            bannerFactory: config.bannerFactory,
156
            secretFactory: config.secretFactory,
157
            tokenFactory: config.tokenFactory,
158
            eventFactory: config.eventFactory,
159
            collectionFactory: config.collectionFactory,
160
            buildClusterFactory: config.buildClusterFactory,
161
            ecosystem: config.ecosystem,
162
            release: config.release,
163
            queueWebhook: config.queueWebhook,
164
            unzipArtifacts: config.unzipArtifactsEnabled
165
        };
166

167
        const bellConfigs = await config.auth.scm.getBellConfiguration();
6✔
168

169
        config.auth.bell = bellConfigs;
6✔
170

171
        if (config.release && config.release.cookieName) {
6!
172
            server.state(config.release.cookieName, {
6✔
173
                path: '/',
174
                ttl: config.release.cookieTimeout * 60 * 1000, // (2 mins)
175
                isSecure: config.auth.https,
176
                isHttpOnly: !config.auth.https
177
            });
178
        }
179

180
        server.ext('onPreResponse', handlePreResponseLogs);
6✔
181

182
        // Audit log
183
        if (config.log && config.log.audit.enabled) {
6!
184
            server.ext('onCredentials', (request, h) => {
×
185
                const { username, scope, pipelineId } = request.auth.credentials;
×
186

187
                if (scope) {
×
188
                    const validScope = config.log.audit.scope.filter(s => scope.includes(s));
×
189

190
                    if (Array.isArray(validScope) && validScope.length > 0) {
×
191
                        let context;
192

193
                        if (validScope.includes('admin')) {
×
194
                            context = `Admin ${username}`;
×
195
                        } else if (validScope.includes('user')) {
×
196
                            context = `User ${username}`;
×
197
                        } else if (validScope.includes('build') || validScope.includes('temporal')) {
×
198
                            context = `Build ${username}`;
×
199
                        } else if (validScope.includes('pipeline')) {
×
200
                            context = `Pipeline ${pipelineId}`;
×
201
                        } else {
202
                            context = `Guest ${username}`;
×
203
                        }
204

205
                        logger.info(`[Login] ${context} ${request.method} ${request.path}`);
×
206
                    }
207
                }
208

209
                return h.continue;
×
210
            });
211
        }
212

213
        // Register events for notifications plugin
214
        server.event(['build_status', 'job_status']);
6✔
215

216
        // Register plugins
217
        await registerPlugins(server, config);
6✔
218

219
        // Initialize common data in buildFactory and jobFactory
220
        server.app.buildFactory.apiUri = server.info.uri;
5✔
221
        server.app.buildFactory.tokenGen = (buildId, metadata, scmContext, expiresIn, scope = ['temporal']) =>
5✔
222
            server.plugins.auth.generateToken(
2✔
223
                server.plugins.auth.generateProfile({ username: buildId, scmContext, scope, metadata }),
224
                expiresIn
225
            );
226
        server.app.buildFactory.executor.tokenGen = server.app.buildFactory.tokenGen;
5✔
227
        server.app.buildFactory.maxDownloadSize = parseInt(config.artifactsMaxDownloadSize, 10) * 1024 * 1024 * 1024;
5✔
228

229
        server.app.jobFactory.apiUri = server.info.uri;
5✔
230
        server.app.jobFactory.tokenGen = (username, metadata, scmContext, scope = ['user']) =>
5✔
231
            server.plugins.auth.generateToken(
2✔
232
                server.plugins.auth.generateProfile({ username, scmContext, scope, metadata })
233
            );
234
        server.app.jobFactory.executor.userTokenGen = server.app.jobFactory.tokenGen;
5✔
235

236
        if (server.plugins.shutdown) {
5!
237
            server.plugins.shutdown.handler({
×
238
                taskname: 'executor-queue-cleanup',
239
                task: async () => {
240
                    await server.app.jobFactory.cleanUp();
×
241
                    logger.info('completed clean up tasks');
×
242
                }
243
            });
244
        }
245

246
        // Start the server
247
        await server.start();
5✔
248

249
        return server;
5✔
250
    } catch (err) {
251
        logger.error('Failed to start server', err);
1✔
252
        throw err;
1✔
253
    }
254
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc