Compare commits
No commits in common. "develop" and "v0.0.15" have entirely different histories.
24 changed files with 4739 additions and 5860 deletions
1
.envrc
1
.envrc
|
@ -1 +0,0 @@
|
||||||
use flake
|
|
|
@ -19,7 +19,6 @@ module.exports = {
|
||||||
'import/no-default-export': 'error',
|
'import/no-default-export': 'error',
|
||||||
'import/no-namespace': 'error',
|
'import/no-namespace': 'error',
|
||||||
'import/no-useless-path-segments': 'error',
|
'import/no-useless-path-segments': 'error',
|
||||||
'import/no-named-as-default': 0,
|
|
||||||
'no-duplicate-imports': 'error',
|
'no-duplicate-imports': 'error',
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
|
|
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
@ -1,2 +1 @@
|
||||||
github: lstrojny
|
github: lstrojny
|
||||||
custom: ["https://paypal.me/larsstrojny"]
|
|
||||||
|
|
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
|
@ -3,11 +3,4 @@ updates:
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: weekly
|
||||||
open-pull-requests-limit: 30
|
|
||||||
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
open-pull-requests-limit: 30
|
|
||||||
|
|
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
|
@ -4,12 +4,16 @@ on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
|
- { node-version: 10.x, lint: false, tests: false }
|
||||||
|
- { node-version: 11.x, lint: false, tests: false }
|
||||||
|
- { node-version: 12.x, lint: false, tests: false }
|
||||||
|
- { node-version: 13.x, lint: false, tests: false }
|
||||||
- { node-version: 14.x, lint: true, tests: true }
|
- { node-version: 14.x, lint: true, tests: true }
|
||||||
- { node-version: 15.x, lint: false, tests: true }
|
- { node-version: 15.x, lint: false, tests: true }
|
||||||
- { node-version: 16.x, lint: true, tests: true }
|
- { node-version: 16.x, lint: true, tests: true }
|
||||||
|
@ -20,16 +24,16 @@ jobs:
|
||||||
name: nodejs ${{ matrix.node-version }} (${{ matrix.lint && 'lint → ' || '' }}${{ matrix.tests && 'test → ' || '' }}build)
|
name: nodejs ${{ matrix.node-version }} (${{ matrix.lint && 'lint → ' || '' }}${{ matrix.tests && 'test → ' || '' }}build)
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.0
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3.8.1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
id: cache-npm
|
id: cache-npm
|
||||||
uses: actions/cache@v3.3.2
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
|
@ -38,7 +42,7 @@ jobs:
|
||||||
|
|
||||||
- name: Cache eslint
|
- name: Cache eslint
|
||||||
id: cache-eslint
|
id: cache-eslint
|
||||||
uses: actions/cache@v3.3.2
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: cache-eslint
|
cache-name: cache-eslint
|
||||||
with:
|
with:
|
||||||
|
@ -47,7 +51,7 @@ jobs:
|
||||||
|
|
||||||
- name: Cache TypeScript
|
- name: Cache TypeScript
|
||||||
id: cache-typescript
|
id: cache-typescript
|
||||||
uses: actions/cache@v3.3.2
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: cache-typescript
|
cache-name: cache-typescript
|
||||||
with:
|
with:
|
||||||
|
@ -56,7 +60,7 @@ jobs:
|
||||||
|
|
||||||
- name: Cache prettier
|
- name: Cache prettier
|
||||||
id: cache-prettier
|
id: cache-prettier
|
||||||
uses: actions/cache@v3.3.2
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: cache-prettier
|
cache-name: cache-prettier
|
||||||
with:
|
with:
|
||||||
|
@ -76,7 +80,7 @@ jobs:
|
||||||
if: ${{ matrix.tests }}
|
if: ${{ matrix.tests }}
|
||||||
|
|
||||||
- name: Upload code coverage
|
- name: Upload code coverage
|
||||||
uses: actions/upload-artifact@v3.1.3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: code-coverage
|
name: code-coverage
|
||||||
path: coverage/lcov.info
|
path: coverage/lcov.info
|
||||||
|
|
28
.github/workflows/dependabot-automerge.yml
vendored
28
.github/workflows/dependabot-automerge.yml
vendored
|
@ -1,28 +0,0 @@
|
||||||
name: Dependabot auto merge
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: [CI]
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
automerge:
|
|
||||||
name: Auto merge "${{ github.event.workflow_run.head_branch }}"
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
if: >
|
|
||||||
github.event.workflow_run.event == 'pull_request'
|
|
||||||
&& github.event.workflow_run.conclusion == 'success'
|
|
||||||
&& github.actor == 'dependabot[bot]'
|
|
||||||
&& startsWith(github.event.workflow_run.head_branch, 'dependabot/')
|
|
||||||
steps:
|
|
||||||
- name: Checkout source
|
|
||||||
uses: actions/checkout@v4.1.0
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.workflow_run.head_commit.id }}
|
|
||||||
|
|
||||||
- name: Instruct @dependabot to merge
|
|
||||||
run: "gh issue comment $ISSUE_ID --body \"(This is an automated comment from workflow $WORKFLOW_URL)\n\n@dependabot squash and merge\""
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.DEPENDABOT_COMMENT_TOKEN }}
|
|
||||||
ISSUE_ID: ${{ github.event.workflow_run.pull_requests[0].number }}
|
|
||||||
WORKFLOW_URL: ${{ github.event.repository.html_url }}/actions/runs/${{ github.run_id }}
|
|
6
.github/workflows/sonar.yml
vendored
6
.github/workflows/sonar.yml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: Sonar scan
|
name: Sonar
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
|
@ -7,12 +7,12 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sonar:
|
sonar:
|
||||||
name: Sonar scan on "${{ github.event.workflow_run.head_branch }}"
|
name: Sonar
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event.workflow_run.conclusion == 'success'
|
if: github.event.workflow_run.conclusion == 'success'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.0
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||||
ref: ${{ github.event.workflow_run.head_branch }}
|
ref: ${{ github.event.workflow_run.head_branch }}
|
||||||
|
|
14
README.md
14
README.md
|
@ -133,22 +133,16 @@ Once *Prometheus* is restarted, metrics with the `homebridge_` prefix should sta
|
||||||
|
|
||||||
|
|
||||||
// Debug
|
// Debug
|
||||||
//
|
|
||||||
// Default: false
|
|
||||||
"debug": "<boolean>",
|
"debug": "<boolean>",
|
||||||
|
|
||||||
|
|
||||||
// Metrics prefix
|
// Metrics prefix
|
||||||
//
|
|
||||||
// Default: "homebridge"
|
|
||||||
"prefix": "<string>",
|
"prefix": "<string>",
|
||||||
|
|
||||||
|
|
||||||
// Metrics server port
|
// Metrics server port
|
||||||
//
|
//
|
||||||
// TCP port where the Prometheus metrics server listens
|
// TCP port where the Prometheus metrics server listens
|
||||||
//
|
|
||||||
// Default: 36123
|
|
||||||
"port": "<integer>",
|
"port": "<integer>",
|
||||||
|
|
||||||
|
|
||||||
|
@ -157,32 +151,24 @@ Once *Prometheus* is restarted, metrics with the `homebridge_` prefix should sta
|
||||||
// Interface where the Prometheus metrics server listens. Can be an IP, a
|
// Interface where the Prometheus metrics server listens. Can be an IP, a
|
||||||
// hostname, "0.0.0.0" for all IPv4 interfaces, "::1" for all IPv6 interfaces.
|
// hostname, "0.0.0.0" for all IPv4 interfaces, "::1" for all IPv6 interfaces.
|
||||||
// Default is "::" which means "any interface"
|
// Default is "::" which means "any interface"
|
||||||
//
|
|
||||||
// Default: "::"
|
|
||||||
"interface": "<string>",
|
"interface": "<string>",
|
||||||
|
|
||||||
|
|
||||||
// Service refresh interval
|
// Service refresh interval
|
||||||
//
|
//
|
||||||
// Discover new services every <interval> seconds
|
// Discover new services every <interval> seconds
|
||||||
//
|
|
||||||
// Default: 60
|
|
||||||
"refresh_interval": "<integer>",
|
"refresh_interval": "<integer>",
|
||||||
|
|
||||||
|
|
||||||
// Request timeout
|
// Request timeout
|
||||||
//
|
//
|
||||||
// Request timeout when interacting with homebridge instances
|
// Request timeout when interacting with homebridge instances
|
||||||
//
|
|
||||||
// Default: 10
|
|
||||||
"request_timeout": "<integer>",
|
"request_timeout": "<integer>",
|
||||||
|
|
||||||
|
|
||||||
// Service discovery timeout
|
// Service discovery timeout
|
||||||
//
|
//
|
||||||
// Discovery timeout after which the current discovery is considered failed
|
// Discovery timeout after which the current discovery is considered failed
|
||||||
//
|
|
||||||
// Default: 20
|
|
||||||
"discovery_timeout": "<integer>",
|
"discovery_timeout": "<integer>",
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
const { parseSchema } = require('json-schema-to-zod')
|
const { parseSchema } = require('json-schema-to-zod-with-defaults')
|
||||||
const { schema } = require('../config.schema.json')
|
const { schema } = require('../config.schema.json')
|
||||||
const { format } = require('prettier')
|
const { format } = require('prettier')
|
||||||
const { join, basename } = require('path')
|
const { join, basename } = require('path')
|
||||||
|
@ -11,7 +11,7 @@ const file = join(__dirname, '../src/generated/config_boundary.ts')
|
||||||
|
|
||||||
console.log(`Starting code generation for ${file}`)
|
console.log(`Starting code generation for ${file}`)
|
||||||
|
|
||||||
const zodSchema = parseSchema(schema, false)
|
const zodSchema = parseSchema(schema, true)
|
||||||
|
|
||||||
const code = format(
|
const code = format(
|
||||||
`
|
`
|
||||||
|
@ -56,9 +56,6 @@ function generateDocs(schema) {
|
||||||
if (definition.description) {
|
if (definition.description) {
|
||||||
lines.push(`//\n// ${wordwrap(definition.description, 80, '\n// ')}`)
|
lines.push(`//\n// ${wordwrap(definition.description, 80, '\n// ')}`)
|
||||||
}
|
}
|
||||||
if (definition.default !== undefined) {
|
|
||||||
lines.push(`//\n// Default: ${JSON.stringify(definition.default)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push(`${JSON.stringify(property)}: ${JSON.stringify('<' + definition.type + '>')}`)
|
lines.push(`${JSON.stringify(property)}: ${JSON.stringify('<' + definition.type + '>')}`)
|
||||||
|
|
||||||
|
|
|
@ -24,9 +24,9 @@ for (const [name, service] of Object.entries(hap.Service)) {
|
||||||
const code = format(
|
const code = format(
|
||||||
`
|
`
|
||||||
// Auto-generated by "${join(basename(__dirname), basename(__filename))}", don’t manually edit
|
// Auto-generated by "${join(basename(__dirname), basename(__filename))}", don’t manually edit
|
||||||
export const Uuids = ${JSON.stringify(uuidToServiceMap)} as const
|
export const Uuids: Record<string,string> = ${JSON.stringify(uuidToServiceMap)} as const
|
||||||
|
|
||||||
export const Services = ${JSON.stringify(serviceToUuidMap)} as const
|
export const Services: Record<string,string> = ${JSON.stringify(serviceToUuidMap)} as const
|
||||||
`,
|
`,
|
||||||
{ filepath: 'codegen.ts', ...prettierConfig },
|
{ filepath: 'codegen.ts', ...prettierConfig },
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
description = "homebridge-prometheus-exporter";
|
description = "Ansible environment";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
10255
package-lock.json
generated
10255
package-lock.json
generated
File diff suppressed because it is too large
Load diff
25
package.json
25
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "homebridge-prometheus-exporter",
|
"name": "homebridge-prometheus-exporter",
|
||||||
"version": "1.0.5",
|
"version": "0.0.15",
|
||||||
"description": "Prometheus exporter for homebridge accessories.",
|
"description": "Prometheus exporter for homebridge accessories.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -16,10 +16,9 @@
|
||||||
},
|
},
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"_portable_exec": "npmPortableExec() { `npm root`/.bin/$@; }; npmPortableExec",
|
"lint": "ifNotCi() { test \"$CI\" && echo \"$2\" || echo \"$1\"; }; `npm bin`/tsc --noEmit && `npm bin`/prettier --ignore-path=.gitignore `ifNotCi --write \"--check --cache --cache-strategy content\"` '**/**.{ts,js,json}' && `npm bin`/eslint `ifNotCi --fix \"--cache --cache-strategy content\"` --ignore-path=.gitignore '**/**.{ts,js,json}'",
|
||||||
"lint": "ifNotCi() { test \"$CI\" && echo \"$2\" || echo \"$1\"; }; npm run _portable_exec -- tsc --noEmit && npm run _portable_exec -- prettier --ignore-path=.gitignore `ifNotCi --write \"--check --cache --cache-strategy content\"` '**/**.{ts,js,json}' && npm run _portable_exec -- eslint `ifNotCi --fix \"--cache --cache-strategy content\"` --ignore-path=.gitignore '**/**.{ts,js,json}'",
|
|
||||||
"start": "npm run build && npm run link && nodemon",
|
"start": "npm run build && npm run link && nodemon",
|
||||||
"test": "ifNotCi() { test \"$CI\" && echo \"$2\" || echo \"$1\"; }; npm run code-generation && npm run _portable_exec -- jest `ifNotCi --watchAll --collect-coverage`",
|
"test": "ifNotCi() { test \"$CI\" && echo \"$2\" || echo \"$1\"; }; npm run code-generation && `npm bin`/jest `ifNotCi --watchAll --collect-coverage`",
|
||||||
"link": "npm install --no-save file:///$PWD/",
|
"link": "npm install --no-save file:///$PWD/",
|
||||||
"build": "rimraf ./dist .tsbuildinfo && npm run code-generation && tsc",
|
"build": "rimraf ./dist .tsbuildinfo && npm run code-generation && tsc",
|
||||||
"code-generation": "./code-generation/hap-gen.js && ./code-generation/config-scheme-gen.js",
|
"code-generation": "./code-generation/hap-gen.js && ./code-generation/config-scheme-gen.js",
|
||||||
|
@ -35,11 +34,10 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "^29.3.0",
|
"@jest/globals": "^29.3.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/node": "^20.2.3",
|
"@types/node": "^18.11.9",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.42.0",
|
"@typescript-eslint/eslint-plugin": "^5.42.0",
|
||||||
"@typescript-eslint/parser": "^5.42.0",
|
"@typescript-eslint/parser": "^5.42.0",
|
||||||
"array.prototype.flatmap": "^1.3.1",
|
|
||||||
"eslint": "^8.0.1",
|
"eslint": "^8.0.1",
|
||||||
"eslint-import-resolver-typescript": "^3.5.2",
|
"eslint-import-resolver-typescript": "^3.5.2",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
@ -48,23 +46,22 @@
|
||||||
"homebridge": "^1.3.5",
|
"homebridge": "^1.3.5",
|
||||||
"homebridge-cmdswitch2": "^0.2.10",
|
"homebridge-cmdswitch2": "^0.2.10",
|
||||||
"jest": "^29.3.0",
|
"jest": "^29.3.0",
|
||||||
"json-schema-to-zod": "^0.6.0",
|
"json-schema-to-zod-with-defaults": "^0.2.1",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^2.0.13",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"release-it": "^16.0.0",
|
"release-it": "^15.5.0",
|
||||||
"rimraf": "^5.0.0",
|
"rimraf": "^3.0.2",
|
||||||
"supertest": "^6.3.1",
|
"supertest": "^6.3.1",
|
||||||
"ts-jest": "^29.0.3",
|
"ts-jest": "^29.0.3",
|
||||||
"ts-node": "^10.3.0",
|
"ts-node": "^10.3.0",
|
||||||
"typescript": "^5.0.2"
|
"typescript": "^4.4.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/auth": "^4.1.0",
|
"@fastify/auth": "^4.1.0",
|
||||||
"@fastify/basic-auth": "^5.0.0",
|
"@fastify/basic-auth": "^4.0.0",
|
||||||
"array.prototype.group": "^1.1.2",
|
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"fastify": "^4.9.2",
|
"fastify": "^4.9.2",
|
||||||
"hap-node-client": "^0.2.1",
|
"hap-node-client": "^0.1.25",
|
||||||
"zod": "^3.19.1"
|
"zod": "^3.19.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,4 +4,3 @@ sonar.sources=.
|
||||||
sonar.exclusions=tests/**
|
sonar.exclusions=tests/**
|
||||||
sonar.tests=tests
|
sonar.tests=tests
|
||||||
sonar.javascript.lcov.reportPaths=./coverage/lcov.info
|
sonar.javascript.lcov.reportPaths=./coverage/lcov.info
|
||||||
sonar.coverage.exclusions=code-generation/**,*.config.js,src/generated/**
|
|
|
@ -21,10 +21,10 @@ export type HttpConfig = Pick<
|
||||||
>
|
>
|
||||||
|
|
||||||
export interface HttpServer {
|
export interface HttpServer {
|
||||||
log: Logger | null
|
log?: Logger
|
||||||
config: HttpConfig
|
config: HttpConfig
|
||||||
serverFactory?: (requestListener: RequestListener) => Server
|
serverFactory?: (requestListener: RequestListener) => Server
|
||||||
onRequest(): HttpResponse | null
|
onRequest(): HttpResponse | undefined
|
||||||
onMetrics(): HttpResponse
|
onMetrics(): HttpResponse
|
||||||
onNotFound(): HttpResponse
|
onNotFound(): HttpResponse
|
||||||
onError(error: Error): HttpResponse
|
onError(error: Error): HttpResponse
|
||||||
|
|
9
src/ambient.d.ts
vendored
9
src/ambient.d.ts
vendored
|
@ -13,12 +13,3 @@ declare module 'hap-node-client' {
|
||||||
on(event: 'Ready', callback: (v: unknown) => void): void
|
on(event: 'Ready', callback: (v: unknown) => void): void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'array.prototype.group' {
|
|
||||||
function shim(): void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Array<T> {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
group<U>(fn: (value: T, index: number, array: T[]) => U, thisArg?: any): { U: T[] }
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Auto-generated by "code-generation/hap-gen.js", don’t manually edit
|
// Auto-generated by "code-generation/hap-gen.js", don’t manually edit
|
||||||
export const Uuids = {
|
export const Uuids: Record<string, string> = {
|
||||||
'00000260-0000-1000-8000-0026BB765291': 'AccessCode',
|
'00000260-0000-1000-8000-0026BB765291': 'AccessCode',
|
||||||
'000000DA-0000-1000-8000-0026BB765291': 'AccessControl',
|
'000000DA-0000-1000-8000-0026BB765291': 'AccessControl',
|
||||||
'0000003E-0000-1000-8000-0026BB765291': 'AccessoryInformation',
|
'0000003E-0000-1000-8000-0026BB765291': 'AccessoryInformation',
|
||||||
|
@ -78,7 +78,7 @@ export const Uuids = {
|
||||||
'0000008C-0000-1000-8000-0026BB765291': 'WindowCovering',
|
'0000008C-0000-1000-8000-0026BB765291': 'WindowCovering',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const Services = {
|
export const Services: Record<string, string> = {
|
||||||
AccessCode: '00000260-0000-1000-8000-0026BB765291',
|
AccessCode: '00000260-0000-1000-8000-0026BB765291',
|
||||||
AccessControl: '000000DA-0000-1000-8000-0026BB765291',
|
AccessControl: '000000DA-0000-1000-8000-0026BB765291',
|
||||||
AccessoryInformation: '0000003E-0000-1000-8000-0026BB765291',
|
AccessoryInformation: '0000003E-0000-1000-8000-0026BB765291',
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import type { Accessory, Device, Service } from './boundaries'
|
import type { Accessory, Device, Service } from './boundaries'
|
||||||
import { Services, Uuids } from './generated/services'
|
import { Uuids } from './generated/services'
|
||||||
import { assertTypeExhausted, isKeyOfConstObject, isType, strCamelCaseToSnakeCase } from './std'
|
import { assertTypeExhausted, isType } from './std'
|
||||||
|
|
||||||
type Labels = Record<string, string>
|
|
||||||
|
|
||||||
export class Metric {
|
export class Metric {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly value: number,
|
public readonly value: number,
|
||||||
public readonly timestamp: Date | null = null,
|
public readonly timestamp: Date | null = null,
|
||||||
public readonly labels: Labels = {},
|
public readonly labels: Record<string, string> = {},
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +35,7 @@ export function aggregate(devices: Device[], timestamp: Date): Metric[] {
|
||||||
return metrics.flat()
|
return metrics.flat()
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractMetrics(service: Service, timestamp: Date, labels: Labels): Metric[] {
|
function extractMetrics(service: Service, timestamp: Date, labels: Record<string, string>) {
|
||||||
const metrics: Metric[] = []
|
const metrics: Metric[] = []
|
||||||
|
|
||||||
for (const characteristic of service.characteristics) {
|
for (const characteristic of service.characteristics) {
|
||||||
|
@ -65,7 +63,7 @@ function extractMetrics(service: Service, timestamp: Date, labels: Labels): Metr
|
||||||
case 'uint64':
|
case 'uint64':
|
||||||
{
|
{
|
||||||
const name = formatName(
|
const name = formatName(
|
||||||
isKeyOfConstObject(service.type, Uuids) ? Uuids[service.type] : 'custom',
|
Uuids[service.type] || 'custom',
|
||||||
characteristic.description,
|
characteristic.description,
|
||||||
characteristic.unit,
|
characteristic.unit,
|
||||||
)
|
)
|
||||||
|
@ -81,35 +79,45 @@ function extractMetrics(service: Service, timestamp: Date, labels: Labels): Metr
|
||||||
return metrics
|
return metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatName(serviceName: string, description: string, unit: string | null = null): string {
|
export function formatName(serviceName: string, description: string, unit: string | undefined = undefined): string {
|
||||||
return (
|
return (
|
||||||
[serviceName, description, typeof unit === 'string' ? unit.toLowerCase() : undefined]
|
[serviceName, description, typeof unit === 'string' ? unit.toLowerCase() : undefined]
|
||||||
.filter(isType('string'))
|
.filter(isType('string'))
|
||||||
.map((val) => strCamelCaseToSnakeCase(val))
|
.map((v) => camelCaseToSnakeCase(v))
|
||||||
// Remove duplicate prefix
|
// Remove duplicate prefix
|
||||||
.reduce((carry, val) => (val.startsWith(carry) ? val : `${carry}_${val}`))
|
.reduce((carry, value) => (value.startsWith(carry) ? value : `${carry}_${value}`))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceLabels(device: Device): Labels {
|
function camelCaseToSnakeCase(str: string): string {
|
||||||
|
return str
|
||||||
|
.replace(/\B([A-Z][a-z])/g, ' $1')
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, '_')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeviceLabels(device: Device): Record<string, string> {
|
||||||
return {
|
return {
|
||||||
bridge: device.instance.name,
|
bridge: device.instance.name,
|
||||||
device_id: device.instance.deviceID,
|
device_id: device.instance.deviceID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAccessoryLabels(accessory: Accessory): Labels {
|
function getAccessoryLabels(accessory: Accessory): Record<string, string> {
|
||||||
|
const labels: Record<string, string> = {}
|
||||||
|
|
||||||
for (const service of accessory.services) {
|
for (const service of accessory.services) {
|
||||||
if (service.type === Services.AccessoryInformation) {
|
if (service.type === '0000003E-0000-1000-8000-0026BB765291') {
|
||||||
return getServiceLabels(service)
|
return getServiceLabels(service)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {}
|
return labels
|
||||||
}
|
}
|
||||||
|
|
||||||
function getServiceLabels(service: Service): Labels {
|
function getServiceLabels(service: Service): Record<string, string> {
|
||||||
const labels: Labels = {}
|
const labels: Record<string, string> = {}
|
||||||
|
|
||||||
for (const characteristic of service.characteristics) {
|
for (const characteristic of service.characteristics) {
|
||||||
if (
|
if (
|
||||||
|
@ -126,7 +134,7 @@ function getServiceLabels(service: Service): Labels {
|
||||||
'Hardware Revision',
|
'Hardware Revision',
|
||||||
].includes(characteristic.description)
|
].includes(characteristic.description)
|
||||||
) {
|
) {
|
||||||
labels[strCamelCaseToSnakeCase(characteristic.description)] = characteristic.value
|
labels[camelCaseToSnakeCase(characteristic.description)] = characteristic.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { type Config, ConfigBoundary, checkBoundary } from './boundaries'
|
||||||
|
|
||||||
export class PrometheusExporterPlatform implements IndependentPlatformPlugin {
|
export class PrometheusExporterPlatform implements IndependentPlatformPlugin {
|
||||||
private readonly httpServer: PrometheusServer
|
private readonly httpServer: PrometheusServer
|
||||||
private httpServerController: HttpServerController | null = null
|
private httpServerController: HttpServerController | undefined = undefined
|
||||||
private readonly config: Config
|
private readonly config: Config
|
||||||
|
|
||||||
constructor(public readonly log: Logger, config: PlatformConfig, public readonly api: API) {
|
constructor(public readonly log: Logger, config: PlatformConfig, public readonly api: API) {
|
||||||
|
|
|
@ -1,39 +1,27 @@
|
||||||
|
import type { Metric } from './metrics'
|
||||||
import type { Logger } from 'homebridge'
|
import type { Logger } from 'homebridge'
|
||||||
import type { HttpConfig, HttpResponse, HttpServer } from './adapters/http'
|
import type { HttpConfig, HttpResponse, HttpServer } from './adapters/http'
|
||||||
import type { Metric } from './metrics'
|
|
||||||
import { strTrimRight } from './std'
|
|
||||||
import { shim } from 'array.prototype.group'
|
|
||||||
shim()
|
|
||||||
|
|
||||||
export class MetricsRenderer {
|
export class MetricsRenderer {
|
||||||
private readonly prefix: string
|
private readonly prefix: string
|
||||||
|
|
||||||
constructor(prefix: string) {
|
constructor(prefix: string) {
|
||||||
this.prefix = strTrimRight(prefix, '_')
|
this.prefix = trimRight(prefix, '_')
|
||||||
}
|
}
|
||||||
|
|
||||||
render(metrics: Metric[]): string {
|
render(metric: Metric): string {
|
||||||
return (
|
const name = this.metricName(metric.name)
|
||||||
Object.entries(metrics.sort().group((metric) => this.metricName(metric.name)))
|
return [
|
||||||
.map(([name, metrics]) => {
|
`# TYPE ${name} ${name.endsWith('_total') ? 'counter' : 'gauge'}`,
|
||||||
return [
|
`${name}${this.renderLabels(metric.labels)} ${metric.value}${
|
||||||
`# TYPE ${name} ${name.endsWith('_total') ? 'counter' : 'gauge'}`,
|
metric.timestamp !== null ? ' ' + String(metric.timestamp.getTime()) : ''
|
||||||
metrics.map((metric) => this.formatMetric(metric)).join('\n'),
|
}`,
|
||||||
].join('\n')
|
].join('\n')
|
||||||
})
|
|
||||||
.join('\n\n') + '\n'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatMetric(metric: Metric): string {
|
private renderLabels(labels: Metric['labels']): string {
|
||||||
return `${this.metricName(metric.name)}${MetricsRenderer.renderLabels(metric.labels)} ${metric.value}${
|
|
||||||
metric.timestamp !== null ? ' ' + String(metric.timestamp.getTime()) : ''
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
|
|
||||||
private static renderLabels(labels: Metric['labels']): string {
|
|
||||||
const rendered = Object.entries(labels)
|
const rendered = Object.entries(labels)
|
||||||
.map(([label, val]) => `${sanitizePrometheusMetricName(label)}="${escapeAttributeValue(val)}"`)
|
.map(([key, value]) => `${sanitizePrometheusMetricName(key)}="${escapeAttributeValue(value)}"`)
|
||||||
.join(',')
|
.join(',')
|
||||||
|
|
||||||
return rendered !== '' ? '{' + rendered + '}' : ''
|
return rendered !== '' ? '{' + rendered + '}' : ''
|
||||||
|
@ -46,6 +34,14 @@ export class MetricsRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trimRight(str: string, char: string): string {
|
||||||
|
return stringReverse(stringReverse(str).replace(new RegExp(`^[${char}]+`), ''))
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringReverse(str: string): string {
|
||||||
|
return str.split('').reverse().join('')
|
||||||
|
}
|
||||||
|
|
||||||
const retryAfterWhileDiscovery = 15
|
const retryAfterWhileDiscovery = 15
|
||||||
const textContentType = 'text/plain; charset=utf-8'
|
const textContentType = 'text/plain; charset=utf-8'
|
||||||
const prometheusSpecVersion = '0.0.4'
|
const prometheusSpecVersion = '0.0.4'
|
||||||
|
@ -61,19 +57,17 @@ export class PrometheusServer implements HttpServer {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly config: HttpConfig,
|
public readonly config: HttpConfig,
|
||||||
public readonly log: Logger | null = null,
|
public readonly log: Logger | undefined = undefined,
|
||||||
private readonly renderer: MetricsRenderer = new MetricsRenderer(config.prefix),
|
private readonly renderer: MetricsRenderer = new MetricsRenderer(config.prefix),
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
onRequest(): HttpResponse | null {
|
onRequest(): HttpResponse | undefined {
|
||||||
if (this.metricsDiscovered) {
|
if (!this.metricsDiscovered) {
|
||||||
return null
|
return {
|
||||||
}
|
statusCode: 503,
|
||||||
|
headers: withHeaders(textContentType, { 'Retry-After': String(retryAfterWhileDiscovery) }),
|
||||||
return {
|
body: 'Metrics discovery pending',
|
||||||
statusCode: 503,
|
}
|
||||||
headers: withHeaders(textContentType, { 'Retry-After': String(retryAfterWhileDiscovery) }),
|
|
||||||
body: 'Metrics discovery pending',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +96,7 @@ export class PrometheusServer implements HttpServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMetricsDiscovery(metrics: Metric[]): void {
|
onMetricsDiscovery(metrics: Metric[]): void {
|
||||||
this.metricsResponse = this.renderer.render(metrics)
|
this.metricsResponse = metrics.map((metric) => this.renderer.render(metric)).join('\n')
|
||||||
this.metricsDiscovered = true
|
this.metricsDiscovered = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,7 +113,7 @@ function escapeString(str: string) {
|
||||||
*
|
*
|
||||||
* `undefined` is converted to an empty string.
|
* `undefined` is converted to an empty string.
|
||||||
*/
|
*/
|
||||||
function escapeAttributeValue(str: Metric['labels'][keyof Metric['labels']]) {
|
function escapeAttributeValue(str: string) {
|
||||||
if (typeof str !== 'string') {
|
if (typeof str !== 'string') {
|
||||||
str = JSON.stringify(str)
|
str = JSON.stringify(str)
|
||||||
}
|
}
|
||||||
|
|
30
src/std.ts
30
src/std.ts
|
@ -1,41 +1,15 @@
|
||||||
|
type Types = 'string' | 'number' | 'boolean' | 'object'
|
||||||
interface TypeMap {
|
interface TypeMap {
|
||||||
string: string
|
string: string
|
||||||
number: number
|
number: number
|
||||||
bigint: bigint
|
|
||||||
boolean: boolean
|
boolean: boolean
|
||||||
object: object
|
object: object
|
||||||
symbol: symbol
|
|
||||||
undefined: undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type predicate higher order function for use with e.g. filter or map
|
export function isType<T extends Types>(type: T): (v: unknown) => v is TypeMap[T] {
|
||||||
export function isType<T extends keyof TypeMap>(type: T): (v: unknown) => v is TypeMap[T] {
|
|
||||||
return (v: unknown): v is TypeMap[T] => typeof v === type
|
return (v: unknown): v is TypeMap[T] => typeof v === type
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type predicate for object keys
|
|
||||||
// Only safe for const objects, as other objects might carry additional, undeclared properties
|
|
||||||
export function isKeyOfConstObject<T extends object>(key: string | number | symbol, obj: T): key is keyof T {
|
|
||||||
return key in obj
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use for exhaustiveness checks in switch/case
|
|
||||||
export function assertTypeExhausted(v: never): never {
|
export function assertTypeExhausted(v: never): never {
|
||||||
throw new Error(`Type should be exhausted but is not. Value "${JSON.stringify(v)}`)
|
throw new Error(`Type should be exhausted but is not. Value "${JSON.stringify(v)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function strCamelCaseToSnakeCase(str: string): string {
|
|
||||||
return str
|
|
||||||
.replace(/\B([A-Z][a-z])/g, ' $1')
|
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
|
||||||
.replace(/\s+/g, '_')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function strReverse(str: string): string {
|
|
||||||
return str.split('').reverse().join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function strTrimRight(str: string, char: string): string {
|
|
||||||
return strReverse(strReverse(str).replace(new RegExp(`^[${char}]+`), ''))
|
|
||||||
}
|
|
||||||
|
|
|
@ -71,10 +71,8 @@ describe('Fastify HTTP adapter', () => {
|
||||||
[
|
[
|
||||||
'# TYPE homebridge_metric gauge',
|
'# TYPE homebridge_metric gauge',
|
||||||
'homebridge_metric{name="metric"} 0.1 1577836800000',
|
'homebridge_metric{name="metric"} 0.1 1577836800000',
|
||||||
'',
|
|
||||||
'# TYPE homebridge_something_total counter',
|
'# TYPE homebridge_something_total counter',
|
||||||
'homebridge_something_total{name="counter"} 100 1577836800000',
|
'homebridge_something_total{name="counter"} 100 1577836800000',
|
||||||
'',
|
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -119,10 +117,8 @@ describe('Fastify HTTP adapter', () => {
|
||||||
[
|
[
|
||||||
'# TYPE homebridge_metric gauge',
|
'# TYPE homebridge_metric gauge',
|
||||||
'homebridge_metric{name="metric"} 0.1 1577836800000',
|
'homebridge_metric{name="metric"} 0.1 1577836800000',
|
||||||
'',
|
|
||||||
'# TYPE homebridge_something_total counter',
|
'# TYPE homebridge_something_total counter',
|
||||||
'homebridge_something_total{name="counter"} 100 1577836800000',
|
'homebridge_something_total{name="counter"} 100 1577836800000',
|
||||||
'',
|
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,94 +6,68 @@ describe('Render prometheus metrics', () => {
|
||||||
const renderer = new MetricsRenderer('prefix')
|
const renderer = new MetricsRenderer('prefix')
|
||||||
|
|
||||||
test('Renders simple metric', () => {
|
test('Renders simple metric', () => {
|
||||||
expect(renderer.render([new Metric('metric', 0.000001)])).toEqual(
|
expect(renderer.render(new Metric('metric', 0.000001))).toEqual(
|
||||||
`# TYPE prefix_metric gauge
|
`# TYPE prefix_metric gauge
|
||||||
prefix_metric 0.000001
|
prefix_metric 0.000001`,
|
||||||
`,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Renders simple metric with timestamp', () => {
|
test('Renders simple metric with timestamp', () => {
|
||||||
expect(renderer.render([new Metric('metric', 0.000001, new Date('2000-01-01 00:00:00 UTC'))])).toEqual(
|
expect(renderer.render(new Metric('metric', 0.000001, new Date('2000-01-01 00:00:00 UTC')))).toEqual(
|
||||||
`# TYPE prefix_metric gauge
|
`# TYPE prefix_metric gauge
|
||||||
prefix_metric 0.000001 946684800000
|
prefix_metric 0.000001 946684800000`,
|
||||||
`,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Renders simple metric with labels', () => {
|
test('Renders simple metric with labels', () => {
|
||||||
expect(
|
expect(
|
||||||
renderer.render([
|
renderer.render(
|
||||||
new Metric('metric', 0.000001, new Date('2000-01-01 00:00:00 UTC'), { label: 'Some Label' }),
|
new Metric('metric', 0.000001, new Date('2000-01-01 00:00:00 UTC'), { label: 'Some Label' }),
|
||||||
]),
|
),
|
||||||
).toEqual(
|
).toEqual(
|
||||||
`# TYPE prefix_metric gauge
|
`# TYPE prefix_metric gauge
|
||||||
prefix_metric{label="Some Label"} 0.000001 946684800000
|
prefix_metric{label="Some Label"} 0.000001 946684800000`,
|
||||||
`,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Renders total as counter', () => {
|
test('Renders total as counter', () => {
|
||||||
for (const metricName of ['some_total_metric', 'some_metric_total', 'total_some_metric']) {
|
for (const metricName of ['some_total_metric', 'some_metric_total', 'total_some_metric']) {
|
||||||
expect(
|
expect(
|
||||||
renderer.render([
|
renderer.render(
|
||||||
new Metric(metricName, 42, new Date('2000-01-01 00:00:00 UTC'), { label: 'Some Label' }),
|
new Metric(metricName, 42, new Date('2000-01-01 00:00:00 UTC'), { label: 'Some Label' }),
|
||||||
]),
|
),
|
||||||
).toEqual(
|
).toEqual(
|
||||||
`# TYPE prefix_some_metric_total counter
|
`# TYPE prefix_some_metric_total counter
|
||||||
prefix_some_metric_total{label="Some Label"} 42 946684800000
|
prefix_some_metric_total{label="Some Label"} 42 946684800000`,
|
||||||
`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Renders multiple metrics correctly', () => {
|
|
||||||
expect(
|
|
||||||
renderer.render([
|
|
||||||
new Metric('some_gauge', 10, new Date('2000-01-01 00:00:00 UTC')),
|
|
||||||
new Metric('another_gauge', 30, new Date('2000-01-01 00:00:00 UTC')),
|
|
||||||
new Metric('some_gauge', 20, new Date('2000-01-01 00:00:00 UTC')),
|
|
||||||
]),
|
|
||||||
).toEqual(
|
|
||||||
`# TYPE prefix_some_gauge gauge
|
|
||||||
prefix_some_gauge 10 946684800000
|
|
||||||
prefix_some_gauge 20 946684800000
|
|
||||||
|
|
||||||
# TYPE prefix_another_gauge gauge
|
|
||||||
prefix_another_gauge 30 946684800000
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Sanitizes metric names', () => {
|
test('Sanitizes metric names', () => {
|
||||||
expect(renderer.render([new Metric('mätric name', 0)])).toEqual(
|
expect(renderer.render(new Metric('mätric name', 0))).toEqual(
|
||||||
`# TYPE prefix_m_tric_name gauge
|
`# TYPE prefix_m_tric_name gauge
|
||||||
prefix_m_tric_name 0
|
prefix_m_tric_name 0`,
|
||||||
`,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Sanitizes label names', () => {
|
test('Sanitizes label names', () => {
|
||||||
expect(renderer.render([new Metric('metric', 0, null, { 'yet another label': 'foo' })])).toEqual(
|
expect(renderer.render(new Metric('metric', 0, null, { 'yet another label': 'foo' }))).toEqual(
|
||||||
`# TYPE prefix_metric gauge
|
`# TYPE prefix_metric gauge
|
||||||
prefix_metric{yet_another_label="foo"} 0
|
prefix_metric{yet_another_label="foo"} 0`,
|
||||||
`,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Escapes newlines in attribute value', () => {
|
test('Escapes newlines in attribute value', () => {
|
||||||
expect(renderer.render([new Metric('metric', 0, null, { label: 'foo\nbar' })])).toEqual(
|
expect(renderer.render(new Metric('metric', 0, null, { label: 'foo\nbar' }))).toEqual(
|
||||||
`# TYPE prefix_metric gauge
|
`# TYPE prefix_metric gauge
|
||||||
prefix_metric{label="foo\\nbar"} 0
|
prefix_metric{label="foo\\nbar"} 0`,
|
||||||
`,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Escapes quotes in attribute value', () => {
|
test('Escapes quotes in attribute value', () => {
|
||||||
expect(renderer.render([new Metric('metric', 0, null, { label: 'foo"bar' })])).toEqual(
|
expect(renderer.render(new Metric('metric', 0, null, { label: 'foo"bar' }))).toEqual(
|
||||||
`# TYPE prefix_metric gauge
|
`# TYPE prefix_metric gauge
|
||||||
prefix_metric{label="foo\\"bar"} 0
|
prefix_metric{label="foo\\"bar"} 0`,
|
||||||
`,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"rootDir": "./",
|
"rootDir": "./",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": false,
|
"importsNotUsedAsValues": "error",
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"tsBuildInfoFile": ".tsbuildinfo",
|
"tsBuildInfoFile": ".tsbuildinfo",
|
||||||
|
|
Loading…
Add table
Reference in a new issue