flightdeck 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flightdeck",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "Jeremy Banka",
@@ -12,6 +12,7 @@
12
12
  "type": "module",
13
13
  "files": [
14
14
  "dist",
15
+ "gen",
15
16
  "src"
16
17
  ],
17
18
  "main": "dist/lib.js",
@@ -22,26 +23,29 @@
22
23
  },
23
24
  "dependencies": {
24
25
  "@t3-oss/env-core": "0.11.1",
25
- "cron": "3.2.1",
26
- "zod": "3.23.8",
27
- "atom.io": "0.30.4",
26
+ "cron": "3.3.1",
27
+ "zod": "3.24.1",
28
+ "atom.io": "0.30.6",
28
29
  "comline": "0.1.6"
29
30
  },
30
31
  "devDependencies": {
31
- "@types/node": "22.9.1",
32
+ "@biomejs/js-api": "0.7.1",
33
+ "@biomejs/wasm-nodejs": "1.9.4",
34
+ "@types/node": "22.10.2",
32
35
  "@types/tmp": "0.2.6",
33
- "bun-types": "1.1.35",
34
- "concurrently": "9.1.0",
36
+ "bun-types": "1.1.42",
37
+ "concurrently": "9.1.1",
38
+ "json-schema-to-zod": "2.6.0",
35
39
  "rimraf": "6.0.1",
36
40
  "tmp": "0.2.3",
37
41
  "tsup": "8.3.5",
38
- "vitest": "2.1.5"
42
+ "vitest": "3.0.0-beta.3",
43
+ "varmint": "0.3.4"
39
44
  },
40
45
  "scripts": {
46
+ "gen": "bun ./__scripts__/gen.bun.ts",
41
47
  "build": "rimraf dist && concurrently \"bun:build:*\" && concurrently \"bun:schema:*\"",
42
- "build:bin:flightdeck": "bun build --outdir dist --target node --external flightdeck --external atom.io --external comline --external zod -- src/flightdeck.bin.ts",
43
- "build:bin:klaxon": "bun build --outdir dist --target node --external flightdeck --external atom.io --external comline --external zod -- src/klaxon.bin.ts",
44
- "build:lib": "bun build --outdir dist --target node --external flightdeck --external atom.io --external comline --external zod -- src/lib.ts ",
48
+ "build:js": "bun ./__scripts__/build.bun.ts",
45
49
  "build:dts": "tsup",
46
50
  "schema:flightdeck": "bun ./src/flightdeck.bin.ts --outdir=dist -- schema",
47
51
  "lint:biome": "biome check -- .",
@@ -3,11 +3,21 @@
3
3
  import * as path from "node:path"
4
4
 
5
5
  import type { OptionsGroup } from "comline"
6
- import { cli, optional, parseNumberOption } from "comline"
6
+ import { cli, optional, parseBooleanOption, parseNumberOption } from "comline"
7
7
  import { z } from "zod"
8
8
 
9
9
  import type { FlightDeckOptions } from "./flightdeck.lib"
10
- import { FlightDeck } from "./flightdeck.lib"
10
+ import { FlightDeck, FlightDeckLogger } from "./flightdeck.lib"
11
+
12
+ const CLI_LOGGER = new FlightDeckLogger(`comline`, process.pid, undefined, {
13
+ jsonLogging: true,
14
+ })
15
+ Object.assign(console, {
16
+ log: CLI_LOGGER.info.bind(CLI_LOGGER),
17
+ info: CLI_LOGGER.info.bind(CLI_LOGGER),
18
+ warn: CLI_LOGGER.warn.bind(CLI_LOGGER),
19
+ error: CLI_LOGGER.error.bind(CLI_LOGGER),
20
+ })
11
21
 
12
22
  const FLIGHTDECK_MANUAL = {
13
23
  optionsSchema: z.object({
@@ -20,6 +30,7 @@ const FLIGHTDECK_MANUAL = {
20
30
  install: z.string(),
21
31
  checkAvailability: z.string(),
22
32
  }),
33
+ jsonLogging: z.boolean().optional(),
23
34
  }),
24
35
  options: {
25
36
  port: {
@@ -55,6 +66,13 @@ const FLIGHTDECK_MANUAL = {
55
66
  example: `--scripts="{\\"download\\":\\"npm i",\\"install\\":\\"npm run build\\"}"`,
56
67
  parse: JSON.parse,
57
68
  },
69
+ jsonLogging: {
70
+ flag: `j`,
71
+ required: false,
72
+ description: `Enable json logging.`,
73
+ example: `--jsonLogging`,
74
+ parse: parseBooleanOption,
75
+ },
58
76
  },
59
77
  } satisfies OptionsGroup<FlightDeckOptions>
60
78
 
@@ -3,13 +3,16 @@ import type { Server } from "node:http"
3
3
  import { createServer } from "node:http"
4
4
  import { homedir } from "node:os"
5
5
  import { resolve } from "node:path"
6
+ import { inspect } from "node:util"
6
7
 
7
8
  import { Future } from "atom.io/internal"
8
9
  import { discoverType } from "atom.io/introspection"
9
10
  import { fromEntries, toEntries } from "atom.io/json"
10
11
  import { ChildSocket } from "atom.io/realtime-server"
11
12
  import { CronJob } from "cron"
13
+ import { z } from "zod"
12
14
 
15
+ import type { LnavFormat } from "../gen/lnav-format-schema.gen"
13
16
  import { FilesystemStorage } from "./filesystem-storage"
14
17
  import { env } from "./flightdeck.env"
15
18
 
@@ -37,6 +40,7 @@ export type FlightDeckOptions<S extends string = string> = {
37
40
  }
38
41
  port?: number | undefined
39
42
  flightdeckRootDir?: string | undefined
43
+ jsonLogging?: boolean | undefined
40
44
  }
41
45
 
42
46
  export class FlightDeck<S extends string = string> {
@@ -61,7 +65,7 @@ export class FlightDeck<S extends string = string> {
61
65
 
62
66
  protected logger: Pick<Console, `error` | `info` | `warn`>
63
67
  protected serviceLoggers: {
64
- readonly [service in S]: Pick<Console, `error` | `info` | `warn`>
68
+ readonly [service in S]: FlightDeckLogger
65
69
  }
66
70
 
67
71
  protected updateAvailabilityChecker: CronJob | null = null
@@ -95,34 +99,21 @@ export class FlightDeck<S extends string = string> {
95
99
  this.servicesReadyToUpdate = { ...this.defaultServicesReadyToUpdate }
96
100
  this.autoRespawnDeadServices = true
97
101
 
98
- this.logger = {
99
- info: (...args: any[]) => {
100
- console.log(`${this.options.packageName}:`, ...args)
101
- },
102
- warn: (...args: any[]) => {
103
- console.warn(`${this.options.packageName}:`, ...args)
104
- },
105
- error: (...args: any[]) => {
106
- console.error(`${this.options.packageName}:`, ...args)
107
- },
108
- }
102
+ this.logger = new FlightDeckLogger(
103
+ this.options.packageName,
104
+ process.pid,
105
+ undefined,
106
+ { jsonLogging: this.options.jsonLogging ?? false },
107
+ )
109
108
  this.serviceLoggers = fromEntries(
110
109
  servicesEntries.map(([serviceName]) => [
111
110
  serviceName,
112
- {
113
- info: (...args: any[]) => {
114
- console.log(`${this.options.packageName}::${serviceName}:`, ...args)
115
- },
116
- warn: (...args: any[]) => {
117
- console.warn(`${this.options.packageName}::${serviceName}:`, ...args)
118
- },
119
- error: (...args: any[]) => {
120
- console.error(
121
- `${this.options.packageName}::${serviceName}:`,
122
- ...args,
123
- )
124
- },
125
- },
111
+ new FlightDeckLogger(
112
+ this.options.packageName,
113
+ process.pid,
114
+ serviceName,
115
+ { jsonLogging: this.options.jsonLogging ?? false },
116
+ ),
126
117
  ]),
127
118
  )
128
119
 
@@ -316,13 +307,15 @@ export class FlightDeck<S extends string = string> {
316
307
  cwd: this.options.flightdeckRootDir,
317
308
  env: import.meta.env,
318
309
  })
319
- this.services[serviceName] = new ChildSocket(
310
+ const serviceLogger = this.serviceLoggers[serviceName]
311
+ const service = (this.services[serviceName] = new ChildSocket(
320
312
  serviceProcess,
321
313
  `${this.options.packageName}::${serviceName}`,
322
- console,
323
- )
314
+ serviceLogger,
315
+ ))
316
+ serviceLogger.processCode = service.process.pid ?? -1
324
317
  this.services[serviceName].onAny((...messages) => {
325
- this.serviceLoggers[serviceName].info(`💬`, ...messages)
318
+ serviceLogger.info(`💬`, ...messages)
326
319
  })
327
320
  this.services[serviceName].on(`readyToUpdate`, () => {
328
321
  this.logger.info(`Service "${serviceName}" is ready to update.`)
@@ -444,3 +437,213 @@ export class FlightDeck<S extends string = string> {
444
437
  }
445
438
  }
446
439
  }
440
+
441
+ export const flightDeckLogSchema = z.object({
442
+ level: z.union([z.literal(`info`), z.literal(`warn`), z.literal(`ERR!`)]),
443
+ timestamp: z.number(),
444
+ package: z.string(),
445
+ service: z.string().optional(),
446
+ process: z.number(),
447
+ body: z.string(),
448
+ })
449
+ export type FlightDeckLog = z.infer<typeof flightDeckLogSchema>
450
+
451
+ const LINE_FORMAT = `line-format` satisfies keyof LnavFormat
452
+ const VALUE = `value` satisfies keyof LnavFormat
453
+
454
+ export type LnavFormatVisualComponent = Exclude<
455
+ Exclude<LnavFormat[`line-format`], undefined>[number],
456
+ string
457
+ >
458
+
459
+ export type LnavFormatBreakdown = Exclude<LnavFormat[`value`], undefined>
460
+ export type MemberOf<T> = T[keyof T]
461
+ export type LnavFormatValueDefinition = MemberOf<LnavFormatBreakdown>
462
+
463
+ export type FlightDeckFormat = {
464
+ [LINE_FORMAT]: (
465
+ | string
466
+ | (LnavFormatVisualComponent & {
467
+ field: keyof FlightDeckLog | `__level__` | `__timestamp__`
468
+ })
469
+ )[]
470
+ [VALUE]: {
471
+ [K in keyof FlightDeckLog]: LnavFormatValueDefinition & {
472
+ kind: FlightDeckLog[K] extends number | undefined
473
+ ? `integer`
474
+ : FlightDeckLog[K] extends string | undefined
475
+ ? `string`
476
+ : never
477
+ }
478
+ }
479
+ }
480
+
481
+ export const FLIGHTDECK_LNAV_FORMAT = {
482
+ title: `FlightDeck Log`,
483
+ description: `Format for events logged by the FlightDeck process manager.`,
484
+ "file-type": `json`,
485
+ "timestamp-field": `timestamp`,
486
+ "subsecond-field": `subsecond`,
487
+ "subsecond-units": `milli`,
488
+ "opid-field": `service`,
489
+ "level-field": `level`,
490
+ level: {
491
+ info: `info`,
492
+ warning: `warn`,
493
+ error: `err!`,
494
+ },
495
+ "ordered-by-time": true,
496
+
497
+ [LINE_FORMAT]: [
498
+ {
499
+ field: `__level__`,
500
+ },
501
+ {
502
+ prefix: ` `,
503
+ field: `__timestamp__`,
504
+ "timestamp-format": `%Y-%m-%dT%H:%M:%S.%L%Z`,
505
+ },
506
+ {
507
+ prefix: ` `,
508
+ field: `package`,
509
+ },
510
+ {
511
+ prefix: `::`,
512
+ field: `service`,
513
+ "default-value": ``,
514
+ },
515
+ {
516
+ prefix: `[`,
517
+ field: `process`,
518
+ suffix: `]`,
519
+ "min-width": 5,
520
+ },
521
+ {
522
+ prefix: `: `,
523
+ field: `body`,
524
+ },
525
+ ],
526
+
527
+ [VALUE]: {
528
+ timestamp: {
529
+ kind: `integer`,
530
+ },
531
+ level: {
532
+ kind: `string`,
533
+ },
534
+ package: {
535
+ kind: `string`,
536
+ },
537
+ service: {
538
+ kind: `string`,
539
+ },
540
+ process: {
541
+ kind: `integer`,
542
+ },
543
+ body: {
544
+ kind: `string`,
545
+ },
546
+ },
547
+ } as const satisfies FlightDeckFormat & LnavFormat
548
+
549
+ export class FlightDeckLogger
550
+ implements Pick<Console, `error` | `info` | `warn`>
551
+ {
552
+ public readonly packageName: string
553
+ public readonly serviceName?: string
554
+ public readonly jsonLogging: boolean
555
+ public processCode: number
556
+ public constructor(
557
+ packageName: string,
558
+ processCode: number,
559
+ serviceName?: string,
560
+ options?: { jsonLogging: boolean },
561
+ ) {
562
+ this.packageName = packageName
563
+ if (serviceName) {
564
+ this.serviceName = serviceName
565
+ }
566
+ this.processCode = processCode
567
+ this.jsonLogging = options?.jsonLogging ?? false
568
+ }
569
+ public info(...messages: unknown[]): void {
570
+ if (this.jsonLogging) {
571
+ const log: FlightDeckLog = {
572
+ timestamp: Date.now(),
573
+ level: `info`,
574
+ process: this.processCode,
575
+ package: this.packageName,
576
+ body: messages
577
+ .map((message) =>
578
+ typeof message === `string`
579
+ ? message
580
+ : inspect(message, false, null, true),
581
+ )
582
+ .join(` `),
583
+ }
584
+ if (this.serviceName) {
585
+ log.service = this.serviceName
586
+ }
587
+ process.stdout.write(JSON.stringify(log) + `\n`)
588
+ } else {
589
+ const source = this.serviceName
590
+ ? `${this.packageName}::${this.serviceName}`
591
+ : this.packageName
592
+ console.log(`${source}:`, ...messages)
593
+ }
594
+ }
595
+
596
+ public warn(...messages: unknown[]): void {
597
+ if (this.jsonLogging) {
598
+ const log: FlightDeckLog = {
599
+ timestamp: Date.now(),
600
+ level: `warn`,
601
+ process: this.processCode,
602
+ package: this.packageName,
603
+ body: messages
604
+ .map((message) =>
605
+ typeof message === `string`
606
+ ? message
607
+ : inspect(message, false, null, true),
608
+ )
609
+ .join(` `),
610
+ }
611
+ if (this.serviceName) {
612
+ log.service = this.serviceName
613
+ }
614
+ process.stdout.write(JSON.stringify(log) + `\n`)
615
+ } else {
616
+ const source = this.serviceName
617
+ ? `${this.packageName}::${this.serviceName}`
618
+ : this.packageName
619
+ console.warn(`${source}:`, ...messages)
620
+ }
621
+ }
622
+
623
+ public error(...messages: unknown[]): void {
624
+ if (this.jsonLogging) {
625
+ const log: FlightDeckLog = {
626
+ timestamp: Date.now() + Math.floor(Math.random() * 1000),
627
+ level: `ERR!`,
628
+ process: this.processCode,
629
+ package: this.packageName,
630
+ body: messages
631
+ .map((message) =>
632
+ typeof message === `string`
633
+ ? message
634
+ : inspect(message, false, null, true),
635
+ )
636
+ .join(` `),
637
+ }
638
+ if (this.serviceName) {
639
+ log.service = this.serviceName
640
+ }
641
+ process.stdout.write(JSON.stringify(log) + `\n`)
642
+ } else {
643
+ const source = this.serviceName
644
+ ? `${this.packageName}::${this.serviceName}`
645
+ : this.packageName
646
+ console.error(`${source}:`, ...messages)
647
+ }
648
+ }
649
+ }