Skip to content

Commit ab80518

Browse files
committed
feat: monorepo setup for logs package
0 parents  commit ab80518

11 files changed

Lines changed: 3880 additions & 0 deletions

File tree

.gitignore

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
node_modules/
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
dist/
13+
14+
# misc
15+
.DS_Store
16+
.rollup.cache
17+
*.pem
18+
19+
# debug
20+
npm-debug.log*
21+
yarn-debug.log*
22+
yarn-error.log*
23+
24+
# local env files
25+
.env*.local
26+
27+
# typescript
28+
*.tsbuildinfo
29+
next-env.d.ts
30+
31+
#turbo
32+
.turbo

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 Neftic Oy
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "root",
3+
"version": "0.0.1",
4+
"description": "Instant Stripe-level observability for your Next.js project",
5+
"repository": "useflytrap/flytrap-logs",
6+
"author": "Rasmus Gustafsson <rasmus@useflytrap.com>",
7+
"license": "MIT",
8+
"scripts": {
9+
"dev": "turbo dev",
10+
"build": "turbo build",
11+
"test": "turbo test",
12+
"lint": "turbo lint",
13+
"release": "turbo release",
14+
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
15+
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache"
16+
},
17+
"devDependencies": {
18+
"prettier": "^2.8.8",
19+
"turbo": "^1.12.4"
20+
}
21+
}

packages/core/package.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "@useflytrap/logs",
3+
"version": "0.0.1",
4+
"description": "Instant Stripe-level observability for your Next.js project",
5+
"repository": "useflytrap/flytrap-logs",
6+
"exports": {
7+
".": {
8+
"types": "./dist/index.d.ts",
9+
"import": "./dist/index.mjs",
10+
"require": "./dist/index.cjs"
11+
}
12+
},
13+
"main": "./dist/index.cjs",
14+
"module": "./dist/index.mjs",
15+
"types": "./dist/index.d.ts",
16+
"files": [
17+
"dist"
18+
],
19+
"author": "Rasmus Gustafsson <rasmus@useflytrap.com>",
20+
"license": "MIT",
21+
"type": "module",
22+
"keywords": [
23+
"typescript",
24+
"logging",
25+
"canonical logging"
26+
],
27+
"scripts": {
28+
"build": "unbuild",
29+
"test": "pnpm test:core && pnpm test:types",
30+
"test:core": "vitest run",
31+
"test:types": "tsc --noEmit",
32+
"test:coverage": "vitest run --coverage",
33+
"lint": "pnpm eslint --fix \"{src,test}/**/*.{js,json,ts}\"",
34+
"prepublishOnly": "pnpm lint",
35+
"release": "np"
36+
},
37+
"dependencies": {
38+
"zod": "^3.22.4"
39+
},
40+
"devDependencies": {
41+
"@types/node": "^18.16.0",
42+
"@typescript-eslint/eslint-plugin": "^7.0.2",
43+
"eslint": "^8.56.0",
44+
"eslint-config-prettier": "^8.8.0",
45+
"eslint-plugin-prettier": "^5.1.3",
46+
"typescript": "^5.3.3",
47+
"typescript-eslint": "^7.0.2",
48+
"unbuild": "^2.0.0",
49+
"vite": "^5.1.4",
50+
"vitest": "^1.3.1"
51+
}
52+
}

packages/core/src/index.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { z } from "zod"
2+
import { baseLogSchema } from "./schemas"
3+
4+
function buildJson<T extends {}>(logs: Array<Partial<T>>) {
5+
const logsObj: Record<string, unknown> = {}
6+
for (let i = 0; i < logs.length; i++) {
7+
Object.entries(logs[i]).forEach(([key, value]) => {
8+
logsObj[key] = value
9+
})
10+
}
11+
return logsObj as T
12+
}
13+
14+
function buildText<T>(logs: Array<Partial<T>>) {
15+
const jsonLine = buildJson(logs)
16+
return Object.entries(jsonLine)
17+
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
18+
.join(" ")
19+
}
20+
21+
export type FlytrapLogsOptions = {
22+
/**
23+
* The format in which to log out the canonical log line.
24+
*
25+
* @default 'json'
26+
*
27+
* The JSON format is as follows:
28+
* @example
29+
* ```json
30+
* {
31+
* "http_status": 200,
32+
* "user_id": "abcd"
33+
* }
34+
* ```
35+
*
36+
* The `text` format looks like this:
37+
* @example
38+
* ```
39+
* http_status=200 user_id=abcd
40+
* ```
41+
*/
42+
format?: "json" | "text"
43+
44+
/**
45+
* Whether to simply flush the logs to stdout / stderr, or to send them to an API.
46+
*
47+
* If `flushMethod` is `'api'`, the log format will default to `'json'`.
48+
*
49+
* If you're using our Logs Vercel Integration, this should be `'stdout'`
50+
*/
51+
flushMethod?: "stdout" | "api"
52+
53+
/**
54+
* If the `flushMethod` is `'api'`, the logs will be sent to this endpoint with a POST request with a JSON payload.
55+
* The schema for the request can be found @TODO
56+
*/
57+
logsEndpoint?: string
58+
59+
/**
60+
* The Flytrap public key used to authenticate your requests.
61+
*/
62+
flytrapPublicKey?: string
63+
}
64+
65+
export type FlushLevel = "error" | "warn" | "log"
66+
67+
export function createFlytrapLogger<T>({
68+
format = "json",
69+
flushMethod = "api",
70+
logsEndpoint = "https://flytrap-production.up.railway.app/api/v1/logs/raw",
71+
flytrapPublicKey,
72+
}: FlytrapLogsOptions = {}) {
73+
const logFormat: FlytrapLogsOptions["format"] =
74+
flushMethod === "api" ? "json" : format
75+
76+
type Log = z.infer<typeof baseLogSchema> & T
77+
const defaultLog = {
78+
type: "request",
79+
path: "PATH_UNDEFINED",
80+
req: null,
81+
req_headers: {},
82+
res: null,
83+
res_headers: {},
84+
http_status: 200,
85+
duration: 0,
86+
method: "GET",
87+
} satisfies z.infer<typeof baseLogSchema>
88+
89+
let logs: Array<Partial<Log>> = [defaultLog as Log]
90+
91+
return {
92+
getContext() {
93+
return logs
94+
},
95+
addContext(context: Partial<Log>) {
96+
logs.push(context)
97+
},
98+
flush(level: FlushLevel = "log") {
99+
try {
100+
const logValue =
101+
logFormat === "text" ? buildText(logs) : buildJson(logs)
102+
103+
if (flushMethod === "stdout") {
104+
console[level](
105+
typeof logValue === "string" ? logValue : JSON.stringify(logValue)
106+
)
107+
}
108+
if (flushMethod === "api") {
109+
fetch(logsEndpoint, {
110+
method: "POST",
111+
body: JSON.stringify(logValue),
112+
headers: new Headers({
113+
"Content-Type": "application/json",
114+
...(flytrapPublicKey && {
115+
Authorization: `Bearer ${flytrapPublicKey}`,
116+
}),
117+
}),
118+
}).then(async (res) => {
119+
if (res.ok === false) {
120+
console.error(
121+
"Flytrap Logs SDK: Failed to save logs to API. Error:"
122+
)
123+
console.error(await res.text())
124+
}
125+
})
126+
}
127+
} catch (error) {
128+
console.error("Flytrap Logs SDK: Error when flushing logs. Error:")
129+
console.error(error)
130+
}
131+
},
132+
}
133+
}

packages/core/src/schemas.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { z } from "zod"
2+
3+
export const vercelLogSchema = z.object({
4+
message: z.string(),
5+
proxy: z.object({
6+
path: z.string(),
7+
method: z.string(),
8+
userAgent: z.array(z.string()),
9+
clientIp: z.string(),
10+
}),
11+
})
12+
13+
export const httpMethods = [
14+
"GET",
15+
"HEAD",
16+
"POST",
17+
"PUT",
18+
"DELETE",
19+
"CONNECT",
20+
"OPTIONS",
21+
"TRACE",
22+
"PATCH",
23+
] as const
24+
25+
export const requestType = z.enum(["request", "action"])
26+
27+
export const baseLogSchema = z
28+
.object({
29+
method: z.enum(httpMethods),
30+
type: requestType,
31+
path: z.string(),
32+
req: z.any(),
33+
req_headers: z.any(),
34+
res: z.any(),
35+
res_headers: z.any(),
36+
http_status: z.number(),
37+
duration: z.number(),
38+
39+
error: z.any().optional(),
40+
user_id: z.string().optional(),
41+
user_email: z.string().optional(),
42+
})
43+
.passthrough()

packages/core/tsconfig.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"target": "esnext",
4+
"module": "esnext",
5+
"moduleResolution": "node",
6+
"esModuleInterop": true,
7+
"strict": true,
8+
"skipLibCheck": true
9+
},
10+
"exclude": ["node_modules"]
11+
}

0 commit comments

Comments
 (0)