mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-10 22:14:48 -05:00
feat(xo-server-openmetrics): include XO tags as tags label (#9971)
Fixes #9628 Plane : XO-2244
This commit is contained in:
@@ -72,6 +72,7 @@
|
||||
- [i18n] Update Czech, Danish, Norwegian, Slovak, Spanish and Swedish translations (PR [#9914](https://github.com/vatesfr/xen-orchestra/pull/9914))
|
||||
- [REST API] `PATCH /rest/v0/vifs/{id}` to update VIF properties (allowed IPs, locking mode, rate limit, TX checksumming) (PR [#9935](https://github.com/vatesfr/xen-orchestra/pull/9935))
|
||||
- [XO 6] Add an Administration tab in the sidebar with a user management menu (PR [#9947](https://github.com/vatesfr/xen-orchestra/pull/9947))
|
||||
- [OpenMetrics] Include XO tags as a `tags` label on host, VM and SR metrics [#9628](https://github.com/vatesfr/xen-orchestra/issues/9628) (PR [#9971](https://github.com/vatesfr/xen-orchestra/pull/9971))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
@@ -129,6 +130,7 @@
|
||||
- xo-server-ipmi-sensors patch
|
||||
- xo-server-load-balancer minor
|
||||
- xo-server-netbox minor
|
||||
- xo-server-openmetrics minor
|
||||
- xo-server-sdn-controller patch
|
||||
- xo-web patch
|
||||
|
||||
|
||||
@@ -613,6 +613,28 @@ All metrics include these labels for filtering:
|
||||
| `target_type` | Object targeted by an alarm: `sr` or `host` |
|
||||
| `package` | XOSTOR-related RPM name for pending updates |
|
||||
| `status` | SMART overall-health string: `PASSED`, `FAILED`, `UNKNOWN` |
|
||||
| `tags` | XO tags of the object, comma-separated and sorted (e.g. `prod,web`) — on host, VM and SR metrics; omitted when the object has no tags |
|
||||
|
||||
##### Filtering by XO tags
|
||||
|
||||
The `tags` label carries every XO tag of the object as a single sorted, comma-separated string. Use a regex matcher to select objects carrying a given tag:
|
||||
|
||||
```
|
||||
# CPU usage of hosts tagged "production"
|
||||
xcp_host_cpu_average{tags=~"(^|.*,)production(,.*|$)"}
|
||||
|
||||
# Exclude VMs tagged "lab" from an alert
|
||||
xcp_vm_cpu_usage{tags!~"(^|.*,)lab(,.*|$)"}
|
||||
```
|
||||
|
||||
This is useful for MSP/multi-tenant scoping (one tag per customer), or to restrict dashboards and alerting rules to tagged subsets of the infrastructure.
|
||||
|
||||
:::warning
|
||||
Two limitations to keep in mind:
|
||||
|
||||
- **Tags containing commas**: tags are joined with commas, so a single tag named `a,b` is indistinguishable from the two tags `a` and `b` in the label value. Avoid commas in tags used for monitoring.
|
||||
- **Series continuity**: adding or removing a tag changes the `tags` label value, which starts a new Prometheus series for all the object's metrics (the previous series goes stale). Alerting rules with a `for:` clause will reset for that object when its tags change. This also applies once when upgrading to the first plugin version exposing this label: objects that already carry tags start new series at that point, while untagged objects are unaffected since the label is omitted for them.
|
||||
:::
|
||||
|
||||
### PromQL Query Examples
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ This plugin creates an HTTP server that exposes a `/metrics` endpoint compatible
|
||||
- VM uptime metric (`xcp_vm_uptime_seconds`) for running VMs
|
||||
- VM metrics: CPU, memory, network, disk, runstate
|
||||
- `is_control_domain` label on all VM metrics to distinguish dom0 from user VMs
|
||||
- `tags` label on host, VM and SR metrics carrying the XO tags of the object (comma-separated, sorted), enabling selective monitoring with regex matchers such as `{tags=~"(^|.*,)production(,.*|$)"}`
|
||||
- VDI disk size metrics: virtual size and physical usage per VDI (`xcp_vdi_virtual_size_bytes`, `xcp_vdi_physical_usage_bytes`)
|
||||
- Bearer token authentication
|
||||
- OpenMetrics format with EOF marker
|
||||
|
||||
@@ -23,6 +23,7 @@ This plugin creates an HTTP server that exposes a `/metrics` endpoint compatible
|
||||
- VM uptime metric (`xcp_vm_uptime_seconds`) for running VMs
|
||||
- VM metrics: CPU, memory, network, disk, runstate
|
||||
- `is_control_domain` label on all VM metrics to distinguish dom0 from user VMs
|
||||
- `tags` label on host, VM and SR metrics carrying the XO tags of the object (comma-separated, sorted), enabling selective monitoring with regex matchers such as `{tags=~"(^|.*,)production(,.*|$)"}`
|
||||
- VDI disk size metrics: virtual size and physical usage per VDI (`xcp_vdi_virtual_size_bytes`, `xcp_vdi_physical_usage_bytes`)
|
||||
- Bearer token authentication
|
||||
- OpenMetrics format with EOF marker
|
||||
|
||||
@@ -76,12 +76,14 @@ export interface VmLabelInfo {
|
||||
power_state: string // VM power state (Running, Paused, Halted, Suspended)
|
||||
pool_id: string
|
||||
pool_name: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export interface HostLabelInfo {
|
||||
name_label: string
|
||||
pifDeviceToNetworkName: Record<string, string> // { "eth0": "Management" }
|
||||
startTime: number | null // Unix timestamp of host boot (from host.startTime)
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export interface SrLabelInfo {
|
||||
@@ -117,6 +119,7 @@ export type SrDataItem = Pick<XoSr, 'uuid' | 'name_label' | 'size' | 'physical_u
|
||||
sr_type: string
|
||||
host_id?: string
|
||||
host_name?: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export interface XoMetricsData {
|
||||
@@ -177,6 +180,7 @@ interface VdiDataPayload {
|
||||
export type HostStatusItem = Pick<XoHost, 'uuid' | 'name_label' | 'power_state' | 'enabled'> & {
|
||||
pool_id: string
|
||||
pool_name: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
interface HostStatusPayload {
|
||||
@@ -186,6 +190,7 @@ interface HostStatusPayload {
|
||||
export type VmStatusItem = Pick<XoVm, 'uuid' | 'name_label' | 'power_state'> & {
|
||||
pool_id: string
|
||||
pool_name: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
interface VmStatusPayload {
|
||||
@@ -1171,6 +1176,7 @@ class OpenMetricsPlugin {
|
||||
size: sr.size,
|
||||
physical_usage: sr.physical_usage,
|
||||
usage: sr.usage,
|
||||
tags: sr.tags,
|
||||
}
|
||||
|
||||
// For local (non-shared) SRs, add host information
|
||||
@@ -1279,6 +1285,7 @@ class OpenMetricsPlugin {
|
||||
enabled: host.enabled,
|
||||
pool_id: host.$poolId,
|
||||
pool_name: poolLabelMap.get(host.$poolId) ?? '',
|
||||
tags: host.tags,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1308,6 +1315,7 @@ class OpenMetricsPlugin {
|
||||
power_state: vm.power_state,
|
||||
pool_id: vm.$poolId,
|
||||
pool_name: poolLabelMap.get(vm.$poolId) ?? '',
|
||||
tags: vm.tags,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1906,6 +1914,7 @@ class OpenMetricsPlugin {
|
||||
power_state: vm.power_state,
|
||||
pool_id: vm.$poolId,
|
||||
pool_name: poolLabelMap.get(vm.$poolId) ?? '',
|
||||
tags: vm.tags,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1923,6 +1932,7 @@ class OpenMetricsPlugin {
|
||||
name_label: host.name_label,
|
||||
pifDeviceToNetworkName,
|
||||
startTime: host.startTime,
|
||||
tags: host.tags,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,12 +85,14 @@ interface VmLabelInfo {
|
||||
power_state: string
|
||||
pool_id: string
|
||||
pool_name: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
interface HostLabelInfo {
|
||||
name_label: string
|
||||
pifDeviceToNetworkName: Record<string, string>
|
||||
startTime: number | null
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
interface SrLabelInfo {
|
||||
|
||||
@@ -745,6 +745,29 @@ const METRIC_PREFIX = 'xcp'
|
||||
/** OpenMetrics prefix for XO management plane metrics */
|
||||
const XO_METRIC_PREFIX = 'xo'
|
||||
|
||||
/**
|
||||
* Serialize XO object tags into the `tags` label value
|
||||
*
|
||||
* @param tags - Raw tag list from the XO object
|
||||
* @returns Comma-separated, sorted tag list (e.g. `"prod,web"`)
|
||||
*/
|
||||
export function serializeTags(tags: readonly string[] | undefined): string {
|
||||
if (tags === undefined || tags.length === 0) {
|
||||
return ''
|
||||
}
|
||||
return [...new Set(tags)]
|
||||
.filter(tag => tag.trim() !== '')
|
||||
.sort()
|
||||
.join(',')
|
||||
}
|
||||
|
||||
function addTagsLabel(labels: Record<string, string>, tags: readonly string[] | undefined): void {
|
||||
const serialized = serializeTags(tags)
|
||||
if (serialized !== '') {
|
||||
labels.tags = serialized
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape special characters in label values.
|
||||
*
|
||||
@@ -816,6 +839,8 @@ export function transformMetric(
|
||||
type: legend.objectType,
|
||||
}
|
||||
|
||||
let objectTags: readonly string[] | undefined
|
||||
|
||||
// Add pool_name from host credentials
|
||||
if (labelContext !== undefined) {
|
||||
const hostCred = labelContext.hosts.find(h => h.poolId === poolId)
|
||||
@@ -830,6 +855,7 @@ export function transformMetric(
|
||||
if (hostInfo.name_label !== '') {
|
||||
labels.host_name = hostInfo.name_label
|
||||
}
|
||||
objectTags = hostInfo.tags
|
||||
|
||||
// For PIF metrics, add network_name
|
||||
if (extractedLabels.interface !== undefined) {
|
||||
@@ -866,6 +892,7 @@ export function transformMetric(
|
||||
labels.vm_name = vmInfo.name_label
|
||||
}
|
||||
labels.is_control_domain = vmInfo.is_control_domain ? 'true' : 'false'
|
||||
objectTags = vmInfo.tags
|
||||
|
||||
// For VBD metrics, add vdi_name and sr_name
|
||||
if (extractedLabels.device !== undefined) {
|
||||
@@ -905,6 +932,7 @@ export function transformMetric(
|
||||
|
||||
// Add extracted labels (device, interface, vif, sr, core)
|
||||
Object.assign(labels, extractedLabels)
|
||||
addTagsLabel(labels, objectTags)
|
||||
|
||||
return {
|
||||
name: `${METRIC_PREFIX}_${definition.openMetricName}`,
|
||||
@@ -1058,6 +1086,8 @@ export function formatSrMetrics(srDataList: SrDataItem[]): FormattedMetric[] {
|
||||
baseLabels.host_name = sr.host_name
|
||||
}
|
||||
|
||||
addTagsLabel(baseLabels, sr.tags)
|
||||
|
||||
// Virtual size (virtual_allocation)
|
||||
metrics.push({
|
||||
name: `${METRIC_PREFIX}_sr_virtual_size_bytes`,
|
||||
@@ -1181,6 +1211,8 @@ export function formatHostStatusMetrics(hostStatusList: HostStatusItem[]): Forma
|
||||
labels.pool_name = host.pool_name
|
||||
}
|
||||
|
||||
addTagsLabel(labels, host.tags)
|
||||
|
||||
metrics.push({
|
||||
name: `${METRIC_PREFIX}_host_status`,
|
||||
help: 'Host status (1 = current state)',
|
||||
@@ -1221,6 +1253,8 @@ export function formatVmStatusMetrics(vmStatusList: VmStatusItem[]): FormattedMe
|
||||
labels.vm_name = vm.name_label
|
||||
}
|
||||
|
||||
addTagsLabel(labels, vm.tags)
|
||||
|
||||
metrics.push({
|
||||
name: `${METRIC_PREFIX}_vm_status`,
|
||||
help: 'VM power state indicator (always 1; current state is carried by the power_state label)',
|
||||
@@ -1699,6 +1733,8 @@ export function formatHostUptimeMetrics(labelContext: LabelContext): FormattedMe
|
||||
labels.host_name = hostInfo.name_label
|
||||
}
|
||||
|
||||
addTagsLabel(labels, hostInfo.tags)
|
||||
|
||||
metrics.push({
|
||||
name: `${METRIC_PREFIX}_host_uptime_seconds`,
|
||||
help: 'Host uptime in seconds since boot',
|
||||
@@ -1755,6 +1791,8 @@ export function formatVmUptimeMetrics(labelContext: LabelContext): FormattedMetr
|
||||
labels.vm_name = vmInfo.name_label
|
||||
}
|
||||
|
||||
addTagsLabel(labels, vmInfo.tags)
|
||||
|
||||
metrics.push({
|
||||
name: `${METRIC_PREFIX}_vm_uptime_seconds`,
|
||||
help: 'VM uptime in seconds since boot',
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
formatXostorClusterMetrics,
|
||||
formatXostorSmartMetrics,
|
||||
formatXostorUpdatesMetrics,
|
||||
serializeTags,
|
||||
type FormattedMetric,
|
||||
type LabelContext,
|
||||
} from './openmetric-formatter.mjs'
|
||||
@@ -3165,3 +3166,367 @@ describe('indexSrUuidTruncations', () => {
|
||||
assert.equal(Object.keys(index).length, 0)
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Tags Label Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('serializeTags', () => {
|
||||
it('should return empty string for undefined or empty input', () => {
|
||||
assert.equal(serializeTags(undefined), '')
|
||||
assert.equal(serializeTags([]), '')
|
||||
})
|
||||
|
||||
it('should join tags with commas', () => {
|
||||
assert.equal(serializeTags(['production', 'web']), 'production,web')
|
||||
})
|
||||
|
||||
it('should sort tags for stable series identity', () => {
|
||||
assert.equal(serializeTags(['web', 'production', 'customer-a']), 'customer-a,production,web')
|
||||
})
|
||||
|
||||
it('should deduplicate tags', () => {
|
||||
assert.equal(serializeTags(['prod', 'prod', 'web']), 'prod,web')
|
||||
})
|
||||
|
||||
it('should filter out empty tags', () => {
|
||||
assert.equal(serializeTags(['', 'prod', '']), 'prod')
|
||||
})
|
||||
|
||||
it('should filter out whitespace-only tags', () => {
|
||||
assert.equal(serializeTags([' ', 'prod', '\t']), 'prod')
|
||||
assert.equal(serializeTags([' ']), '')
|
||||
})
|
||||
|
||||
it('should collapse a list of identical tags to a single entry', () => {
|
||||
assert.equal(serializeTags(['prod', 'prod', 'prod']), 'prod')
|
||||
})
|
||||
})
|
||||
|
||||
describe('tags label on RRD metrics (transformMetric)', () => {
|
||||
const createTaggedLabelContext = (hostTags?: string[], vmTags?: string[]): LabelContext => ({
|
||||
hosts: [
|
||||
{
|
||||
hostId: 'host-uuid-123',
|
||||
hostAddress: '192.168.1.1',
|
||||
hostLabel: 'Host 1',
|
||||
poolId: 'pool-456',
|
||||
poolLabel: 'Production Pool',
|
||||
sessionId: 'session-123',
|
||||
protocol: 'https:',
|
||||
},
|
||||
],
|
||||
labels: {
|
||||
vms: {
|
||||
'vm-uuid-789': {
|
||||
name_label: 'Web Server',
|
||||
is_control_domain: false,
|
||||
vbdDeviceToVdiName: {},
|
||||
vbdDeviceToVdiUuid: {},
|
||||
vifIndexToNetworkName: {},
|
||||
startTime: null,
|
||||
power_state: 'Running',
|
||||
pool_id: 'pool-456',
|
||||
pool_name: 'Production Pool',
|
||||
tags: vmTags,
|
||||
},
|
||||
},
|
||||
hosts: {
|
||||
'host-uuid-123': {
|
||||
name_label: 'Host 1',
|
||||
pifDeviceToNetworkName: {},
|
||||
startTime: null,
|
||||
tags: hostTags,
|
||||
},
|
||||
},
|
||||
srs: {},
|
||||
srTruncatedToUuid: {},
|
||||
vdiUuidToSrUuid: {},
|
||||
},
|
||||
})
|
||||
|
||||
const hostMetric: ParsedMetric = {
|
||||
legend: {
|
||||
cf: 'AVERAGE',
|
||||
objectType: 'host',
|
||||
uuid: 'host-uuid-123',
|
||||
metricName: 'cpu_avg',
|
||||
rawLegend: 'AVERAGE:host:host-uuid-123:cpu_avg',
|
||||
},
|
||||
value: 0.75,
|
||||
timestamp: 1700000000,
|
||||
}
|
||||
|
||||
const vmMetric: ParsedMetric = {
|
||||
legend: {
|
||||
cf: 'AVERAGE',
|
||||
objectType: 'vm',
|
||||
uuid: 'vm-uuid-789',
|
||||
metricName: 'cpu_usage',
|
||||
rawLegend: 'AVERAGE:vm:vm-uuid-789:cpu_usage',
|
||||
},
|
||||
value: 0.5,
|
||||
timestamp: 1700000000,
|
||||
}
|
||||
|
||||
it('should add tags label to host metrics', () => {
|
||||
const result = transformMetric(hostMetric, 'pool-456', createTaggedLabelContext(['prod', 'dc1']))
|
||||
|
||||
assert.ok(result)
|
||||
assert.equal(result.labels.tags, 'dc1,prod')
|
||||
})
|
||||
|
||||
it('should add tags label to VM metrics', () => {
|
||||
const result = transformMetric(vmMetric, 'pool-456', createTaggedLabelContext(undefined, ['customer-a', 'web']))
|
||||
|
||||
assert.ok(result)
|
||||
assert.equal(result.labels.tags, 'customer-a,web')
|
||||
})
|
||||
|
||||
it('should omit tags label when host has no tags', () => {
|
||||
const result = transformMetric(hostMetric, 'pool-456', createTaggedLabelContext([]))
|
||||
|
||||
assert.ok(result)
|
||||
assert.equal(result.labels.tags, undefined)
|
||||
})
|
||||
|
||||
it('should omit tags label when VM tags are undefined', () => {
|
||||
const result = transformMetric(vmMetric, 'pool-456', createTaggedLabelContext())
|
||||
|
||||
assert.ok(result)
|
||||
assert.equal(result.labels.tags, undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('tags label on status, uptime and SR metrics', () => {
|
||||
it('should add tags label to host status metrics', () => {
|
||||
const hosts: HostStatusItem[] = [
|
||||
{
|
||||
uuid: 'host-1',
|
||||
name_label: 'Host 1',
|
||||
power_state: 'Running',
|
||||
enabled: true,
|
||||
pool_id: 'pool-1',
|
||||
pool_name: 'Pool',
|
||||
tags: ['prod', 'dc1'],
|
||||
},
|
||||
]
|
||||
|
||||
const result = formatHostStatusMetrics(hosts)
|
||||
|
||||
assert.equal(result[0]!.labels.tags, 'dc1,prod')
|
||||
})
|
||||
|
||||
it('should omit tags label on host status metrics without tags', () => {
|
||||
const hosts: HostStatusItem[] = [
|
||||
{
|
||||
uuid: 'host-1',
|
||||
name_label: 'Host 1',
|
||||
power_state: 'Running',
|
||||
enabled: true,
|
||||
pool_id: 'pool-1',
|
||||
pool_name: 'Pool',
|
||||
},
|
||||
]
|
||||
|
||||
const result = formatHostStatusMetrics(hosts)
|
||||
|
||||
assert.equal(result[0]!.labels.tags, undefined)
|
||||
})
|
||||
|
||||
it('should add tags label to VM status metrics', () => {
|
||||
const vms: VmStatusItem[] = [
|
||||
{
|
||||
uuid: 'vm-1',
|
||||
name_label: 'VM 1',
|
||||
power_state: 'Running',
|
||||
pool_id: 'pool-1',
|
||||
pool_name: 'Pool',
|
||||
tags: ['web', 'customer-a'],
|
||||
},
|
||||
]
|
||||
|
||||
const result = formatVmStatusMetrics(vms)
|
||||
|
||||
assert.equal(result[0]!.labels.tags, 'customer-a,web')
|
||||
})
|
||||
|
||||
it('should add tags label to all SR capacity metrics', () => {
|
||||
const srs: SrDataItem[] = [
|
||||
{
|
||||
uuid: 'sr-1',
|
||||
name_label: 'Local Storage',
|
||||
size: 1000,
|
||||
physical_usage: 500,
|
||||
usage: 250,
|
||||
pool_id: 'pool-1',
|
||||
pool_name: 'Pool',
|
||||
sr_type: 'lvm',
|
||||
tags: ['fast', 'ssd'],
|
||||
},
|
||||
]
|
||||
|
||||
const metrics = formatSrMetrics(srs)
|
||||
|
||||
assert.equal(metrics.length, 3)
|
||||
for (const m of metrics) {
|
||||
assert.equal(m.labels.tags, 'fast,ssd')
|
||||
}
|
||||
})
|
||||
|
||||
it('should add tags label to host uptime metrics', () => {
|
||||
const labelContext: LabelContext = {
|
||||
hosts: [
|
||||
{
|
||||
hostId: 'host-1',
|
||||
hostAddress: '192.168.1.1',
|
||||
hostLabel: 'Host 1',
|
||||
poolId: 'pool-456',
|
||||
poolLabel: 'Production Pool',
|
||||
sessionId: 'session-123',
|
||||
protocol: 'https:',
|
||||
},
|
||||
],
|
||||
labels: {
|
||||
vms: {},
|
||||
hosts: {
|
||||
'host-1': {
|
||||
name_label: 'Host 1',
|
||||
pifDeviceToNetworkName: {},
|
||||
startTime: Math.floor(Date.now() / 1000) - 7200,
|
||||
tags: ['prod'],
|
||||
},
|
||||
},
|
||||
srs: {},
|
||||
srTruncatedToUuid: {},
|
||||
vdiUuidToSrUuid: {},
|
||||
},
|
||||
}
|
||||
|
||||
const metrics = formatHostUptimeMetrics(labelContext)
|
||||
|
||||
assert.equal(metrics.length, 1)
|
||||
assert.equal(metrics[0]!.labels.tags, 'prod')
|
||||
})
|
||||
|
||||
it('should add tags label to VM uptime metrics', () => {
|
||||
const labelContext: LabelContext = {
|
||||
hosts: [],
|
||||
labels: {
|
||||
vms: {
|
||||
'vm-1': {
|
||||
name_label: 'VM 1',
|
||||
is_control_domain: false,
|
||||
vbdDeviceToVdiName: {},
|
||||
vbdDeviceToVdiUuid: {},
|
||||
vifIndexToNetworkName: {},
|
||||
startTime: Math.floor(Date.now() / 1000) - 3600,
|
||||
power_state: 'Running',
|
||||
pool_id: 'pool-456',
|
||||
pool_name: 'Production Pool',
|
||||
tags: ['web', 'customer-a'],
|
||||
},
|
||||
},
|
||||
hosts: {},
|
||||
srs: {},
|
||||
srTruncatedToUuid: {},
|
||||
vdiUuidToSrUuid: {},
|
||||
},
|
||||
}
|
||||
|
||||
const metrics = formatVmUptimeMetrics(labelContext)
|
||||
|
||||
assert.equal(metrics.length, 1)
|
||||
assert.equal(metrics[0]!.labels.tags, 'customer-a,web')
|
||||
})
|
||||
|
||||
it('should escape special characters in tag values through formatToOpenMetrics', () => {
|
||||
const vms: VmStatusItem[] = [
|
||||
{
|
||||
uuid: 'vm-1',
|
||||
name_label: 'VM 1',
|
||||
power_state: 'Running',
|
||||
pool_id: 'pool-1',
|
||||
pool_name: 'Pool',
|
||||
tags: ['with"quote', 'with\\backslash'],
|
||||
},
|
||||
]
|
||||
|
||||
const output = formatToOpenMetrics(formatVmStatusMetrics(vms))
|
||||
|
||||
assert.match(output, /tags="with\\"quote,with\\\\backslash"/)
|
||||
})
|
||||
|
||||
it('should escape newlines in tag values through formatToOpenMetrics', () => {
|
||||
const vms: VmStatusItem[] = [
|
||||
{
|
||||
uuid: 'vm-1',
|
||||
name_label: 'VM 1',
|
||||
power_state: 'Running',
|
||||
pool_id: 'pool-1',
|
||||
pool_name: 'Pool',
|
||||
tags: ['line1\nline2'],
|
||||
},
|
||||
]
|
||||
|
||||
const output = formatToOpenMetrics(formatVmStatusMetrics(vms))
|
||||
|
||||
// The newline must be escaped as the two characters `\` + `n`
|
||||
assert.match(output, /tags="line1\\nline2"/)
|
||||
assert.ok(!output.includes('tags="line1\nline2"'))
|
||||
})
|
||||
|
||||
it('should keep tags label on VBD metrics where extracted labels are added afterwards', () => {
|
||||
const labelContext: LabelContext = {
|
||||
hosts: [
|
||||
{
|
||||
hostId: 'host-uuid-123',
|
||||
hostAddress: '192.168.1.1',
|
||||
hostLabel: 'Host 1',
|
||||
poolId: 'pool-456',
|
||||
poolLabel: 'Production Pool',
|
||||
sessionId: 'session-123',
|
||||
protocol: 'https:',
|
||||
},
|
||||
],
|
||||
labels: {
|
||||
vms: {
|
||||
'vm-uuid-789': {
|
||||
name_label: 'Web Server',
|
||||
is_control_domain: false,
|
||||
vbdDeviceToVdiName: { xvda: 'System Disk' },
|
||||
vbdDeviceToVdiUuid: { xvda: 'vdi-uuid-system' },
|
||||
vifIndexToNetworkName: {},
|
||||
startTime: null,
|
||||
power_state: 'Running',
|
||||
pool_id: 'pool-456',
|
||||
pool_name: 'Production Pool',
|
||||
tags: ['prod'],
|
||||
},
|
||||
},
|
||||
hosts: {},
|
||||
srs: {},
|
||||
srTruncatedToUuid: {},
|
||||
vdiUuidToSrUuid: {},
|
||||
},
|
||||
}
|
||||
|
||||
const metric: ParsedMetric = {
|
||||
legend: {
|
||||
cf: 'AVERAGE',
|
||||
objectType: 'vm',
|
||||
uuid: 'vm-uuid-789',
|
||||
metricName: 'vbd_xvda_read',
|
||||
rawLegend: 'AVERAGE:vm:vm-uuid-789:vbd_xvda_read',
|
||||
},
|
||||
value: 1000000,
|
||||
timestamp: 1700000000,
|
||||
}
|
||||
|
||||
const result = transformMetric(metric, 'pool-456', labelContext)
|
||||
|
||||
assert.ok(result)
|
||||
assert.equal(result.labels.device, 'xvda')
|
||||
assert.equal(result.labels.tags, 'prod')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user