Skip to content

Commit 70f208d

Browse files
authored
feat: add CLI support (#7)
1 parent 5feec9e commit 70f208d

File tree

5 files changed

+255
-4
lines changed

5 files changed

+255
-4
lines changed

cli.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/usr/bin/env node
2+
'use strict'
3+
const { pipeline } = require('node:stream')
4+
const { readFileSync } = require('node:fs')
5+
const { join } = require('node:path')
6+
7+
const minimist = require('minimist')
8+
const CloudPine = require('./index')
9+
10+
function parseOptions (argv) {
11+
const opts = {
12+
logName: argv.name,
13+
cloudLoggingOptions: {
14+
sync: argv.sync,
15+
skipInit: argv['skip-init'],
16+
googleCloudOptions: {
17+
projectId: argv.projectId,
18+
keyFilename: argv.key
19+
},
20+
resource: {
21+
type: argv.resource,
22+
labels: {}
23+
},
24+
defaultLabels: {}
25+
}
26+
}
27+
28+
if (argv['resource-labels'] != null) {
29+
const plainLabels = Array.isArray(argv['resource-labels'])
30+
? argv['resource-labels']
31+
: [argv['resource-labels']]
32+
33+
for (const label of plainLabels) {
34+
const [key, value] = label.split('=')
35+
opts.cloudLoggingOptions.resource.labels[key] = value
36+
}
37+
}
38+
39+
if (argv.labels != null) {
40+
const plainLabels = Array.isArray(argv.labels) ? argv.labels : [argv.labels]
41+
42+
for (const label of plainLabels) {
43+
const [key, value] = label.split('=')
44+
opts.cloudLoggingOptions.defaultLabels[key] = value
45+
}
46+
}
47+
48+
return opts
49+
}
50+
51+
async function handler (fgs) {
52+
let rs, rj
53+
const promise = new Promise(
54+
// eslint-ignore-next-line no-return-assign
55+
(resolve, reject) => {
56+
rs = resolve
57+
rj = reject
58+
}
59+
)
60+
61+
if (fgs.version) {
62+
const { version } = JSON.parse(
63+
readFileSync(join(__dirname, 'package.json'), 'utf-8')
64+
)
65+
console.log(`cloud-pine@v${version}`)
66+
67+
rs()
68+
} else if (fgs.help) {
69+
console.log(readFileSync(join(__dirname, 'help.txt'), 'utf-8'))
70+
71+
rs()
72+
} else {
73+
const options = parseOptions(fgs)
74+
75+
pipeline(process.stdin, await CloudPine(options), err => {
76+
if (err) rj(err)
77+
})
78+
}
79+
80+
return promise
81+
}
82+
83+
const flags = minimist(process.argv.slice(2), {
84+
alias: {
85+
name: 'n',
86+
version: 'v',
87+
help: 'h',
88+
sync: 's',
89+
projectId: 'p',
90+
key: 'k',
91+
labels: 'l',
92+
resource: 'r',
93+
'resource-labels': 'rs',
94+
'skip-init': 'i'
95+
},
96+
boolean: ['skip-init', 'sync'],
97+
default: {
98+
sync: true,
99+
'skip-init': false
100+
}
101+
})
102+
103+
handler(flags).then(null, console.error)

help.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Cloud Pine
2+
3+
Forward pino logs to  Google Cloud Logging:
4+
5+
cat log | cloud-pine --projectid someprojectid -l something=else -l service=http
6+
7+
Flags
8+
-h | --help Display Help
9+
-v | --version Display Version
10+
-n | --name Log Name. Default to Cloud_Pine
11+
-s | --sync Cloud Logging Mode. Sync will print to stdout meanwhile async will
12+
forward logs to Cloud Logging
13+
-p | --projectid Google Cloud Project ID. Default to automatic detected resource or
14+
GOOGLE_APPLICATION_CREDENTIALS
15+
-k | --key Path to key file
16+
-l | --labels Custom labels to be attached to the logging labels. Should be in the format label=value.
17+
Can be used one or more times.
18+
-r | --resource Monitoring Resource type. Default to global or Monitored Resource detected.
19+
-rs | --resource-labels Monitoring Resource#Labels that will be attached to the resource by default. Follows same pattern as --labels.
20+
-i | --skip-init Skips identification of monitored resource, which will infer things like project-ide. and Monitored Resource
21+
settings. Default to false.

lib/cloud-logging.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class CloudLogging {
1111
options.defaultLabels
1212
)
1313
this._log = null
14+
this._skipInit = options.skipInit ?? false
1415
// Indicates whether the logs will be forward to stdout or
1516
// through networking to GCP
1617
this._sync = options.sync ?? false
@@ -32,8 +33,10 @@ class CloudLogging {
3233
}
3334

3435
async init () {
35-
await this.logging.setProjectId()
36-
await this.logging.setDetectedResource()
36+
if (this._skipInit === false) {
37+
await this.logging.setProjectId()
38+
await this.logging.setDetectedResource()
39+
}
3740

3841
const labels = Object.assign(
3942
{},

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
},
5050
"dependencies": {
5151
"@google-cloud/logging": "^10.1.1",
52+
"minimist": "^1.2.6",
5253
"split2": "^4.1.0"
5354
},
5455
"tsd": {

test/unit/cloud_logging.test.js

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ tap.test('CloudLogging#sync', root => {
5252
})
5353

5454
root.test('#init', nested => {
55-
nested.plan(2)
55+
nested.plan(3)
5656

5757
nested.test(
5858
'Should initialize the log instance with resources',
@@ -173,6 +173,68 @@ tap.test('CloudLogging#sync', root => {
173173
t.same(instance.resource, expectedDetectedResource)
174174
}
175175
)
176+
177+
nested.test(
178+
'Should be skip if skipInit flag is set',
179+
async t => {
180+
const logName = 'test'
181+
const projectId = 'test-project'
182+
const detectedResource = {
183+
labels: {
184+
projectId,
185+
instanceId: '12345678901234',
186+
zone: 'us-central1-a'
187+
}
188+
}
189+
const defaultOptions = {
190+
googleCloudOptions: {
191+
projectId,
192+
keyFilename: '/path/to/keyfile.json'
193+
},
194+
sync: true,
195+
skipInit: true
196+
}
197+
198+
class LoggingMock extends BaseLoggingMock {
199+
constructor (options) {
200+
t.same(options, defaultOptions.googleCloudOptions)
201+
super()
202+
}
203+
204+
setProjectId () {
205+
t.fail('setProjectId should not be called')
206+
return Promise.resolve()
207+
}
208+
209+
setDetectedResource () {
210+
t.fail('setDetectedResource should not be called')
211+
this.detectedResource = detectedResource
212+
return Promise.resolve()
213+
}
214+
215+
logSync (name) {
216+
t.equal(name, logName)
217+
return new BaseLogMock(name)
218+
}
219+
}
220+
221+
const expectedDetectedResource = Object.assign(
222+
{ type: 'global' },
223+
detectedResource
224+
)
225+
const { CloudLogging } = t.mock('../../lib/cloud-logging', {
226+
'@google-cloud/logging': { Logging: LoggingMock }
227+
})
228+
229+
t.plan(4)
230+
231+
const instance = new CloudLogging(logName, defaultOptions)
232+
233+
await instance.init()
234+
t.ok(instance.sync)
235+
t.notSame(instance.resource, expectedDetectedResource)
236+
}
237+
)
176238
})
177239

178240
root.test('#parseLine', nested => {
@@ -676,7 +738,7 @@ tap.test('CloudLogging#async', root => {
676738
})
677739

678740
root.test('#init', nested => {
679-
nested.plan(2)
741+
nested.plan(3)
680742

681743
nested.test(
682744
'Should initialize the log instance with resources',
@@ -793,6 +855,67 @@ tap.test('CloudLogging#async', root => {
793855
t.same(instance.resource, expectedDetectedResource)
794856
}
795857
)
858+
859+
nested.test(
860+
'Should be skip if skipInit flag is set',
861+
async t => {
862+
const logName = 'test'
863+
const projectId = 'test-project'
864+
const detectedResource = {
865+
labels: {
866+
projectId,
867+
instanceId: '12345678901234',
868+
zone: 'us-central1-a'
869+
}
870+
}
871+
const defaultOptions = {
872+
googleCloudOptions: {
873+
projectId,
874+
keyFilename: '/path/to/keyfile.json'
875+
},
876+
skipInit: true
877+
}
878+
879+
class LoggingMock extends BaseLoggingMock {
880+
constructor (options) {
881+
t.same(options, defaultOptions.googleCloudOptions)
882+
super()
883+
}
884+
885+
setProjectId () {
886+
t.fail('setProjectId should not be called')
887+
return Promise.resolve()
888+
}
889+
890+
setDetectedResource () {
891+
t.fail('setDetectedResource should not be called')
892+
this.detectedResource = detectedResource
893+
return Promise.resolve()
894+
}
895+
896+
log (name) {
897+
t.equal(name, logName)
898+
return new BaseLogMock(name)
899+
}
900+
}
901+
902+
const expectedDetectedResource = Object.assign(
903+
{ type: 'global' },
904+
detectedResource
905+
)
906+
const { CloudLogging } = t.mock('../../lib/cloud-logging', {
907+
'@google-cloud/logging': { Logging: LoggingMock }
908+
})
909+
910+
t.plan(4)
911+
912+
const instance = new CloudLogging(logName, defaultOptions)
913+
914+
await instance.init()
915+
t.notOk(instance.sync)
916+
t.notSame(instance.resource, expectedDetectedResource)
917+
}
918+
)
796919
})
797920

798921
root.test('#parseLine', nested => {

0 commit comments

Comments
 (0)