flightdeck 0.2.4 → 0.2.6

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.4",
3
+ "version": "0.2.6",
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.5",
26
+ "cron": "3.3.2",
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.10.1",
32
+ "@biomejs/js-api": "0.7.1",
33
+ "@biomejs/wasm-nodejs": "1.9.4",
34
+ "@types/node": "22.10.4",
32
35
  "@types/tmp": "0.2.6",
33
- "bun-types": "1.1.38",
34
- "concurrently": "9.1.0",
36
+ "bun-types": "1.1.42",
37
+ "concurrently": "9.1.2",
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": "3.0.0-beta.1"
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,214 @@ 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
+ prefix: ` `,
500
+ field: `__timestamp__`,
501
+ "timestamp-format": `%Y-%m-%dT%H:%M:%S.%L%Z`,
502
+ },
503
+ {
504
+ prefix: ` `,
505
+ field: `process`,
506
+ "min-width": 5,
507
+ },
508
+ {
509
+ prefix: `:`,
510
+ field: `package`,
511
+ },
512
+ {
513
+ prefix: `:`,
514
+ field: `service`,
515
+ "default-value": ``,
516
+ },
517
+ {
518
+ prefix: `[`,
519
+ field: `__level__`,
520
+ suffix: `]`,
521
+ },
522
+ {
523
+ prefix: `: `,
524
+ field: `body`,
525
+ },
526
+ ],
527
+
528
+ [VALUE]: {
529
+ timestamp: {
530
+ kind: `integer`,
531
+ },
532
+ level: {
533
+ kind: `string`,
534
+ },
535
+ package: {
536
+ kind: `string`,
537
+ },
538
+ service: {
539
+ kind: `string`,
540
+ },
541
+ process: {
542
+ kind: `integer`,
543
+ },
544
+ body: {
545
+ kind: `string`,
546
+ },
547
+ },
548
+ } as const satisfies FlightDeckFormat & LnavFormat
549
+
550
+ export class FlightDeckLogger
551
+ implements Pick<Console, `error` | `info` | `warn`>
552
+ {
553
+ public readonly packageName: string
554
+ public readonly serviceName?: string
555
+ public readonly jsonLogging: boolean
556
+ public processCode: number
557
+ public constructor(
558
+ packageName: string,
559
+ processCode: number,
560
+ serviceName?: string,
561
+ options?: { jsonLogging: boolean },
562
+ ) {
563
+ this.packageName = packageName
564
+ if (serviceName) {
565
+ this.serviceName = serviceName
566
+ }
567
+ this.processCode = processCode
568
+ this.jsonLogging = options?.jsonLogging ?? false
569
+ }
570
+ public info(...messages: unknown[]): void {
571
+ if (this.jsonLogging) {
572
+ const log: FlightDeckLog = {
573
+ timestamp: Date.now(),
574
+ level: `info`,
575
+ process: this.processCode,
576
+ package: this.packageName,
577
+ body: messages
578
+ .map((message) =>
579
+ typeof message === `string`
580
+ ? message
581
+ : inspect(message, false, null, true),
582
+ )
583
+ .join(` `),
584
+ }
585
+ if (this.serviceName) {
586
+ log.service = this.serviceName
587
+ }
588
+ process.stdout.write(JSON.stringify(log) + `\n`)
589
+ } else {
590
+ const source = this.serviceName
591
+ ? `${this.packageName}::${this.serviceName}`
592
+ : this.packageName
593
+ console.log(`${source}:`, ...messages)
594
+ }
595
+ }
596
+
597
+ public warn(...messages: unknown[]): void {
598
+ if (this.jsonLogging) {
599
+ const log: FlightDeckLog = {
600
+ timestamp: Date.now(),
601
+ level: `warn`,
602
+ process: this.processCode,
603
+ package: this.packageName,
604
+ body: messages
605
+ .map((message) =>
606
+ typeof message === `string`
607
+ ? message
608
+ : inspect(message, false, null, true),
609
+ )
610
+ .join(` `),
611
+ }
612
+ if (this.serviceName) {
613
+ log.service = this.serviceName
614
+ }
615
+ process.stdout.write(JSON.stringify(log) + `\n`)
616
+ } else {
617
+ const source = this.serviceName
618
+ ? `${this.packageName}::${this.serviceName}`
619
+ : this.packageName
620
+ console.warn(`${source}:`, ...messages)
621
+ }
622
+ }
623
+
624
+ public error(...messages: unknown[]): void {
625
+ if (this.jsonLogging) {
626
+ const log: FlightDeckLog = {
627
+ timestamp: Date.now() + Math.floor(Math.random() * 1000),
628
+ level: `ERR!`,
629
+ process: this.processCode,
630
+ package: this.packageName,
631
+ body: messages
632
+ .map((message) =>
633
+ typeof message === `string`
634
+ ? message
635
+ : inspect(message, false, null, true),
636
+ )
637
+ .join(` `),
638
+ }
639
+ if (this.serviceName) {
640
+ log.service = this.serviceName
641
+ }
642
+ process.stdout.write(JSON.stringify(log) + `\n`)
643
+ } else {
644
+ const source = this.serviceName
645
+ ? `${this.packageName}::${this.serviceName}`
646
+ : this.packageName
647
+ console.error(`${source}:`, ...messages)
648
+ }
649
+ }
650
+ }