-
Notifications
You must be signed in to change notification settings - Fork 7
feat: add storeLog in Config
#21
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
Changes from 8 commits
7139dcf
c6d9f43
55287d4
c9a9c5e
1623e2c
6cb9e05
9fb9682
fbf76e9
52cf014
9564d0a
e1537a5
8237aad
2768d50
af35675
fed0caf
eb5a87d
a611ae4
243356f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,4 @@ | ||
| node_modules/ | ||
| build/ | ||
| build/ | ||
| .DS_Store | ||
| *.log |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,8 @@ import MockServerHandler from "./common/mockHandler"; | |
| import IConfigFetcher from "../interfaces/configFetcherInterface"; | ||
| import storageService from "../services/storageService"; | ||
| import { MockServerResponse } from "../types"; | ||
| import ILogSink from "../interfaces/logSinkInterface"; | ||
| import { HarMiddleware } from "../middlewares/har"; | ||
| import { cleanupPath } from "./utils"; | ||
|
|
||
| interface MockServerConfig { | ||
|
|
@@ -15,14 +17,16 @@ interface MockServerConfig { | |
| class MockServer { | ||
| config: MockServerConfig; | ||
| configFetcher: IConfigFetcher; | ||
| logSink: ILogSink; | ||
| app: Express | ||
|
|
||
| constructor (port: number = 3000, configFetcher: IConfigFetcher, pathPrefix: string = "") { | ||
| constructor (port: number = 3000, configFetcher: IConfigFetcher, logSink: ILogSink, pathPrefix: string = "") { | ||
| this.config = { | ||
| port, | ||
| pathPrefix | ||
| }; | ||
| this.configFetcher = configFetcher; | ||
| this.logSink = logSink; | ||
|
|
||
| this.app = this.setup(); | ||
| } | ||
|
|
@@ -37,6 +41,12 @@ class MockServer { | |
| this.initStorageService(); | ||
|
|
||
| const app = express(); | ||
|
|
||
| // Use middleware to parse `application/json` and `application/x-www-form-urlencoded` body data | ||
| app.use(express.json()); | ||
| app.use(express.urlencoded({ extended: true })); | ||
|
wrongsahil marked this conversation as resolved.
|
||
|
|
||
| app.use(HarMiddleware); | ||
|
|
||
| app.use((_, res, next) => { | ||
| res.set({ | ||
|
|
@@ -73,15 +83,18 @@ class MockServer { | |
| } | ||
|
|
||
| const mockResponse: MockServerResponse = await MockServerHandler.handleEndpoint(req); | ||
| // console.debug("[Debug] Final Mock Response", mockResponse); | ||
| return res.status(mockResponse.statusCode).set(mockResponse.headers).end(mockResponse.body); | ||
| console.debug("[Debug] Final Mock Response", mockResponse); | ||
|
|
||
| res.locals.metadata = mockResponse.metadata; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lets namespace metadata -> rq_metadata for scoping Requestly variables |
||
| return res.status(mockResponse.statusCode).set(mockResponse.headers).send(mockResponse.body); | ||
| }); | ||
|
|
||
| return app; | ||
| } | ||
|
|
||
| initStorageService = () => { | ||
| storageService.setConfigFetcher(this.configFetcher); | ||
| storageService.setLogSink(this.logSink); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import type { | ||
| Request as HarRequest, | ||
| Response as HarResponse, | ||
| Header as HarHeader, | ||
| } from "har-format"; | ||
| import { IncomingHttpHeaders, OutgoingHttpHeaders } from "http"; | ||
| import { Request, Response } from "express"; | ||
| import { RequestMethod } from "../../types"; | ||
|
|
||
| export const getHarHeaders = (headers: IncomingHttpHeaders | OutgoingHttpHeaders): HarHeader[] => { | ||
| const harHeaders: HarHeader[] = []; | ||
|
|
||
| for (const headerName in headers) { | ||
| const headerValue = headers[headerName]; | ||
| // Header values can be string | string[] according to Node.js typings, | ||
| // but HAR format requires a string, so we need to handle this. | ||
| if (headerValue) { | ||
| const value = Array.isArray(headerValue) ? headerValue.join('; ') : headerValue; | ||
| harHeaders.push({ name: headerName, value: value.toString() }); | ||
| } | ||
| } | ||
|
|
||
| return harHeaders; | ||
| }; | ||
|
|
||
| export const getPostData = (req: Request): HarRequest['postData'] => { | ||
| if ([RequestMethod.POST, RequestMethod.PUT, RequestMethod.PATCH].includes(req.method as RequestMethod)) { | ||
| const postData: any = { | ||
| mimeType: req.get('Content-Type') || 'application/json', | ||
| text: '', | ||
| params: [], | ||
| }; | ||
|
|
||
| // When the body is URL-encoded, the body should be converted into params | ||
| if (postData.mimeType === 'application/x-www-form-urlencoded' && typeof req.body === 'object') { | ||
| postData.params = Object.keys(req.body).map(key => ({ | ||
| name: key, | ||
| value: req.body[key], | ||
| })); | ||
| } else if (req.body) { | ||
| try { | ||
| postData.text = typeof req.body === 'string' ? req.body : JSON.stringify(req.body); | ||
| } catch (error) { | ||
| postData.text = ""; | ||
| } | ||
| } | ||
|
|
||
| return postData; | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| export const getHarRequestQueryString = (req: Request): HarRequest['queryString'] => { | ||
| const queryObject: Request['query'] = req.query; | ||
|
|
||
| const queryString: HarRequest['queryString'] = []; | ||
|
|
||
| for (const [name, value] of Object.entries(queryObject)) { | ||
| if (Array.isArray(value)) { | ||
| value.forEach(val => queryString.push({ name, value: val as string })); | ||
| } else { | ||
| queryString.push({ name, value: value as string }); | ||
| } | ||
| } | ||
|
|
||
| return queryString; | ||
| } | ||
|
|
||
| export const buildHarRequest = (req: Request): HarRequest => { | ||
| const requestData = getPostData(req) | ||
| return { | ||
| method: req.method, | ||
| url: req.url, | ||
| httpVersion: req.httpVersion, | ||
| cookies: [], | ||
| headers: getHarHeaders(req.headers), | ||
| queryString: getHarRequestQueryString(req), | ||
| postData: requestData, | ||
| headersSize: -1, // not calculating for now | ||
| bodySize: requestData ? Buffer.byteLength(requestData.text!) : -1, | ||
| } | ||
| }; | ||
|
|
||
| export const buildHarResponse = (res: Response, metadata?: any): HarResponse => { | ||
| const { body } = metadata; | ||
| const bodySize = body ? Buffer.byteLength(JSON.stringify(body || {})) : -1; | ||
| return { | ||
| status: res.statusCode, | ||
| statusText: res.statusMessage, | ||
| httpVersion: res.req.httpVersion, | ||
| cookies: [], | ||
| headers: getHarHeaders(res.getHeaders()), | ||
| content: { | ||
| size: bodySize, // same as bodySize since serving uncompressed | ||
| mimeType: res.get('Content-Type') || 'application/json', | ||
| text: JSON.stringify(body), | ||
| }, | ||
| redirectURL: '', // todo: implement when we integrate rules to mocks | ||
| headersSize: -1, // not calculating for now | ||
| bodySize, | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,14 @@ | ||
| import IConfigFetcher from "./interfaces/configFetcherInterface"; | ||
| import IlogSink from "./interfaces/logSinkInterface"; | ||
| import MockServer from "./core/server"; | ||
| import { Mock as MockSchema, MockMetadata as MockMetadataSchema, Response as MockResponseSchema } from "./types/mock"; | ||
|
|
||
| import {Log as MockLog} from "./types"; | ||
| export { | ||
| MockServer, | ||
| IConfigFetcher, | ||
| IlogSink, | ||
| MockSchema, | ||
| MockMetadataSchema, | ||
| MockResponseSchema, | ||
| MockLog, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { Log } from "../types"; | ||
|
|
||
| class ILogSink { | ||
| store = async (log: Log): Promise<void> => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let rename this to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it would be better to address this one's these changes are merged here
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. addressed it in backend implementation here |
||
| return; | ||
| } | ||
| } | ||
|
|
||
| export default ILogSink; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import type { Entry } from "har-format"; | ||
| import { NextFunction, Request, Response } from "express"; | ||
| import storageService from "../services/storageService"; | ||
| import { buildHarRequest, buildHarResponse } from "../core/utils/harFormatter"; | ||
|
|
||
|
|
||
| export const HarMiddleware = (req: Request, res: Response, next: NextFunction) => { | ||
| const originalSend = res.send; | ||
|
|
||
| const requestStartTime = new Date(); | ||
| const requestStartTimeStamp: string = requestStartTime.toISOString(); | ||
|
|
||
| let responseBody: string; | ||
|
|
||
| res.send = function (body) { | ||
| responseBody = body; | ||
| return originalSend.call(this, body); | ||
| }; | ||
|
|
||
| res.once('finish', () => { | ||
| const HarEntry: Partial<Entry> = { | ||
| time: Date.now() - requestStartTime.getTime(), | ||
| startedDateTime: requestStartTimeStamp, | ||
| request: buildHarRequest(req), | ||
| response: buildHarResponse(res, { body: responseBody }), | ||
| } | ||
|
|
||
| storageService.storeLog({ mockId: res.locals.metadata.mockId, HarEntry, }) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Faced a crash here while running locally
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I faced the same error once. Probably because of the order in which we are applying the metadata in It suprisingly went away when I restarted the server and tested with the backend |
||
| }); | ||
|
|
||
| next(); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import fs from 'fs'; | ||
|
|
||
| import ILogSink from "../interfaces/logSinkInterface"; | ||
| import { Log } from "../types"; | ||
|
|
||
|
|
||
| class FileLogSink implements ILogSink { | ||
| store = async (log: Log): Promise<void> => { | ||
| const logLine = `${JSON.stringify(log.HarEntry)}\n`; | ||
| fs.writeFile(`${log.mockId}.log`, logLine, { flag: 'a+' }, (err) => { | ||
| if(err) { | ||
| console.log("Error dumping log to file."); | ||
| throw err; | ||
| } | ||
| }); | ||
| Promise.resolve(); | ||
| } | ||
| } | ||
|
|
||
| const fileLogSink = new FileLogSink(); | ||
| export default fileLogSink; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import MockServer from "../core/server"; | ||
| import firebaseConfigFetcher from "./firebaseConfigFetcher"; | ||
| import fileLogSink from "./FileLogSink"; | ||
|
|
||
| const server = new MockServer(3001, firebaseConfigFetcher, "/mocksv2"); | ||
| console.log(server.app); | ||
| const server = new MockServer(3001, firebaseConfigFetcher, fileLogSink, "/mocksv2"); | ||
| console.debug(server.app); | ||
| server.start(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logSink should be optional. If not given, should default to empty Sink
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The api of the constructor has been updated in the other PR