Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ logs / traces / metrics into a local query cache and optional Parquet
exports. Everything runs on the local machine — no central server is
required for V1.

> Part of **[HypStack](https://hypstack.ai/)**, an open-source stack for AI observability.

## Quickstart

```sh
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ export class IdentityClient {
async bootstrap() {
const token = this.bootstrapToken
if (typeof token !== 'string' || token.length === 0) {
throw new Error('identity bootstrap failed: identity.bootstrap_token is not set')
throw new Error(
'identity bootstrap failed: identity.bootstrap_token is not set. Run `hyp join <central-url> <token>` to enroll this host, or remove the central sink if you only capture locally'
)
}
const url = joinUrl(this.centralUrl, '/v1/identity/bootstrap')
const body = JSON.stringify({ bootstrap_token: token })
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@
"smoke": "node ./hypaware-core/smoke/index.js"
},
"dependencies": {
"@aws-sdk/client-s3": "3.1074.0",
"@aws-sdk/client-s3": "3.1075.0",
"@aws-sdk/credential-provider-ini": "3.972.56",
"hyparquet": "1.26.1",
"hyparquet-compressors": "1.1.1",
"icebird": "0.8.10",
"icebird": "0.8.11",
"squirreling": "0.12.24"
},
"optionalDependencies": {
Expand All @@ -51,7 +51,7 @@
"squirreling": "0.12.24"
},
"devDependencies": {
"@types/node": "26.0.0",
"@types/node": "26.0.1",
"typescript": "6.0.3"
}
}
2 changes: 1 addition & 1 deletion src/core/cli/core_commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -2250,7 +2250,7 @@ async function runVersion(_argv, ctx) {
const require = createRequire(import.meta.url)
const { version } = require('../../../package.json')
const { hypHome } = readObservabilityEnv(ctx.env)
ctx.stdout.write(`hyp ${version}\n`)
ctx.stdout.write(`hypaware ${version}\n`)
ctx.stdout.write(` node: ${process.version}\n`)
ctx.stdout.write(` platform: ${process.platform} ${process.arch}\n`)
ctx.stdout.write(` hyp_home: ${hypHome}\n`)
Expand Down
25 changes: 24 additions & 1 deletion src/core/cli/dispatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ import { materializeSinks } from '../sinks/materialize.js'
const HELP_FLAGS = new Set(['--help', '-h', 'help'])
const VERSION_FLAGS = new Set(['--version', '-V'])

/**
* Map a sink materialization failure to a one-line, actionable hint.
* The raw `[errorKind]: message` line stays for operators; this adds a
* "what do I do about it" line for the common, confusing cases: a host
* that hasn't joined a fleet, and the expected-but-noisy plugin-not-active
* warning that read-only commands emit because they don't load sink
* writer plugins.
*
* @param {{ instance: string, errorKind: string, message: string }} err
* @returns {string | undefined}
*/
function sinkWarningHint(err) {
if (/bootstrap_token is not set/.test(err.message)) {
return "this host hasn't joined a fleet. Run `hyp join <central-url> <token>` to enable the central sink, or ignore this warning if you only capture locally"
}
if (err.errorKind === 'sink_plugin_not_active') {
return `expected for read-only commands (the writer/destination plugin for '${err.instance}' isn't loaded here); the running daemon is unaffected`
}
return undefined
}

/**
* Boot the kernel CLI and dispatch `argv` to a registered command.
*
Expand Down Expand Up @@ -88,7 +109,7 @@ export async function dispatch(argv, opts = {}) {
if (argv.length > 0 && VERSION_FLAGS.has(argv[0])) {
const require = createRequire(import.meta.url)
const { version } = require('../../../package.json')
stdout.write(`hyp ${version}\n`)
stdout.write(`hypaware ${version}\n`)
return 0
}
if (argv.length > 0 && HELP_FLAGS.has(argv[0])) {
Expand Down Expand Up @@ -132,6 +153,8 @@ export async function dispatch(argv, opts = {}) {
stderr.write(
`warning: sink '${err.instance}' not materialized [${err.errorKind}]: ${err.message}\n`
)
const hint = sinkWarningHint(err)
if (hint) stderr.write(` → ${hint}\n`)
}
}

Expand Down
11 changes: 10 additions & 1 deletion src/core/commands/backfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,14 @@ export async function runBackfill(argv, ctx) {
if (selected.providers.length === 0) {
if (parsed.json) {
ctx.stdout.write(JSON.stringify({ run_id: devRunId, providers: [] }, null, 2) + '\n')
} else if (ctx.backfills.list().length > 0) {
ctx.stdout.write(
'No backfill providers matched your active config. Providers are registered but none are enabled here. Run `hyp backfill list` to see them, or name one explicitly (e.g. `hyp backfill claude`).\n'
)
} else {
ctx.stdout.write('No backfill providers selected. Run `hyp backfill list` for the registered set.\n')
ctx.stdout.write(
'No backfill providers registered. No active plugin contributes one; check enabled plugins with `hyp daemon status`, and if you just joined a fleet the config may still be syncing.\n'
)
}
return 0
}
Expand Down Expand Up @@ -157,6 +163,9 @@ export async function runBackfillList(argv, ctx) {
}
if (providers.length === 0) {
ctx.stdout.write('No backfill providers registered.\n')
ctx.stdout.write(
'No active plugin contributes a backfill provider. Check enabled plugins with `hyp daemon status`; if you just joined a fleet, the config may still be syncing.\n'
)
return 0
}
ctx.stdout.write('Backfill providers:\n')
Expand Down
22 changes: 21 additions & 1 deletion src/core/daemon/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,26 @@ function clampTickInterval(value) {
return Math.max(value, MIN_TICK_INTERVAL_MS)
}

/**
* Turn a raw source-start error into an operator-actionable message.
* The common failure is a port collision (EADDRINUSE): a second
* HypAware daemon or an unrelated service already holds the gateway
* port. The bare Node string ("listen EADDRINUSE ... 127.0.0.1:8787")
* doesn't say what to do, so this appends the remedy.
*
* @param {unknown} err
* @param {string} source
* @returns {string}
*/
function describeSourceStartError(err, source) {
const base = err instanceof Error ? err.message : String(err)
if (/EADDRINUSE/.test(base)) {
const addr = base.match(/[\d.]+:\d+/)?.[0] ?? 'its configured address'
return `${base}. Source '${source}' could not bind ${addr}; another process (a second HypAware daemon or an unrelated service) already holds it. Stop that process or change the listen address, then restart the daemon.`
}
return base
}

/**
* Start every registered source that has not auto-started during
* `activate()`. Returns one snapshot per source — including the
Expand Down Expand Up @@ -612,7 +632,7 @@ async function startConfiguredSources({ runtime, log, fileLog, sourcePluginByNam
details,
})
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = describeSourceStartError(err, contribution.name)
fileLog.error('daemon.source_start_failed', {
source: contribution.name,
plugin,
Expand Down
8 changes: 4 additions & 4 deletions test/core/cli-version.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,22 @@ test('hyp --version prints version and exits 0', async () => {
const opts = makeDispatchOpts()
const code = await dispatch(['--version'], opts)
assert.equal(code, 0)
assert.equal(opts.stdout.text(), `hyp ${pkg.version}\n`)
assert.equal(opts.stdout.text(), `hypaware ${pkg.version}\n`)
})

test('hyp -V prints version and exits 0', async () => {
const opts = makeDispatchOpts()
const code = await dispatch(['-V'], opts)
assert.equal(code, 0)
assert.equal(opts.stdout.text(), `hyp ${pkg.version}\n`)
assert.equal(opts.stdout.text(), `hypaware ${pkg.version}\n`)
})

test('hyp version prints version info and exits 0', async () => {
const opts = makeDispatchOpts()
const code = await dispatch(['version'], opts)
assert.equal(code, 0)
const out = opts.stdout.text()
assert.ok(out.startsWith(`hyp ${pkg.version}\n`))
assert.ok(out.startsWith(`hypaware ${pkg.version}\n`))
assert.ok(out.includes('node:'))
assert.ok(out.includes('platform:'))
assert.ok(out.includes('hyp_home:'))
Expand All @@ -69,6 +69,6 @@ test('version string matches package.json', async () => {
const opts = makeDispatchOpts()
const code = await dispatch(['--version'], opts)
assert.equal(code, 0)
const version = opts.stdout.text().trim().replace('hyp ', '')
const version = opts.stdout.text().trim().replace('hypaware ', '')
assert.equal(version, pkg.version)
})