Skip to content

Commit

Permalink
feat(shared,jmx): support serialising long to string in JSON response…
Browse files Browse the repository at this point in the history
… from Jolokia

* Upgrade jolokia.js to 2.0.3 (that supports 'serializeLong' option)
* Create JMX preferences and provide 'Serialize long to string' option,
  which can affect Jolokia requests within JMX plugin

This fix limits the 'serializeLong=string' option to JMX, because the
change of the bahaviour might cause unexpected errors in other plugins.
JMX is a generic MBeans manipulation plugin, so it should make sense to
have such an option plugin-wide.

Each plugin should decide whether it's worth applying the option or not.

Fix #292
  • Loading branch information
tadayosi committed Jun 8, 2024
1 parent 875c578 commit 83c71a1
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 32 deletions.
2 changes: 1 addition & 1 deletion packages/hawtio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"@types/react-router-dom": "^5.3.3",
"dagre": "^0.8.5",
"eventemitter3": "^5.0.1",
"jolokia.js": "^2.0.1",
"jolokia.js": "^2.0.3",
"jquery": "^3.7.1",
"js-logger": "^1.6.1",
"jwt-decode": "^4.0.0",
Expand Down
31 changes: 31 additions & 0 deletions packages/hawtio/src/plugins/jmx/JmxPreferences.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { JmxOptions, jmxPreferencesService } from '@hawtiosrc/plugins/shared/jmx-preferences-service'
import { TooltipHelpIcon } from '@hawtiosrc/ui/icons'
import { CardBody, Checkbox, Form, FormGroup } from '@patternfly/react-core'
import React, { useState } from 'react'

export const JmxPreferences: React.FunctionComponent = () => {
const [options, setOptions] = useState(jmxPreferencesService.loadOptions())

const updateOptions = (updated: Partial<JmxOptions>) => {
jmxPreferencesService.saveOptions(updated)
setOptions({ ...options, ...updated })
}

return (
<CardBody>
<Form isHorizontal>
<FormGroup
label='Serialize long to string'
fieldId='serialize-long-to-string'
labelIcon={<TooltipHelpIcon tooltip='Serialize long values in the JSON responses from Jolokia to string' />}
>
<Checkbox
id='serialize-long-to-string-input'
isChecked={options.serializeLong}
onChange={(_event, serializeLong) => updateOptions({ serializeLong })}
/>
</FormGroup>
</Form>
</CardBody>
)
}
3 changes: 3 additions & 0 deletions packages/hawtio/src/plugins/jmx/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { hawtio, HawtioPlugin } from '@hawtiosrc/core'
import { helpRegistry } from '@hawtiosrc/help/registry'
import { workspace } from '@hawtiosrc/plugins/shared'
import { preferencesRegistry } from '@hawtiosrc/preferences'
import { pluginPath } from './globals'
import help from './help.md'
import { Jmx } from './Jmx'
import { JmxPreferences } from './JmxPreferences'

const order = 13

Expand All @@ -17,4 +19,5 @@ export const jmx: HawtioPlugin = () => {
isActive: async () => workspace.hasMBeans(),
})
helpRegistry.add('jmx', 'JMX', help, order)
preferencesRegistry.add('jmx', 'JMX', JmxPreferences, order)
}
2 changes: 1 addition & 1 deletion packages/hawtio/src/plugins/logs/logs-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export interface ILogsService {
filter(logs: LogEntry[], filter: LogFilter): LogEntry[]

loadOptions(): LogsOptions
saveOptions(value: Partial<LogsOptions>): void
saveOptions(options: Partial<LogsOptions>): void
}

class LogsService implements ILogsService {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Jolokia, { ListRequestOptions, Request, Response } from 'jolokia.js'
import { AttributeValues, IJolokiaService, JolokiaListMethod, JolokiaStoredOptions } from '../jolokia-service'
import jmxCamelResponse from './jmx-camel-tree.json'
import { OptimisedJmxDomains } from '../tree'
import jmxCamelResponse from './jmx-camel-tree.json'

class MockJolokiaService implements IJolokiaService {
constructor() {
Expand Down Expand Up @@ -45,6 +45,10 @@ class MockJolokiaService implements IJolokiaService {
return null
}

async writeAttribute(mbean: string, attribute: string, value: unknown): Promise<unknown> {
return null
}

async execute(mbean: string, operation: string, args?: unknown[]): Promise<unknown> {
return {}
}
Expand Down
30 changes: 22 additions & 8 deletions packages/hawtio/src/plugins/shared/attributes/attribute-service.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
import { eventService } from '@hawtiosrc/core'
import { rbacService } from '@hawtiosrc/plugins/rbac/rbac-service'
import { AttributeValues, jolokiaService } from '@hawtiosrc/plugins/shared/jolokia-service'
import { escapeMBean } from '@hawtiosrc/util/jolokia'
import { Request, Response } from 'jolokia.js'
import { Request, RequestOptions, Response } from 'jolokia.js'
import { log } from '../globals'
import { rbacService } from '@hawtiosrc/plugins/rbac/rbac-service'
import { eventService } from '@hawtiosrc/core'
import { jmxPreferencesService } from '../jmx-preferences-service'

class AttributeService {
private handles: number[] = []

private requestOptions(): RequestOptions {
const { serializeLong } = jmxPreferencesService.loadOptions()
return serializeLong ? { serializeLong: 'string' } : {}
}

private setupConfig(request: Request): Request {
const { serializeLong } = jmxPreferencesService.loadOptions()
if (serializeLong) {
request.config = { ...request.config, serializeLong: 'string' }
}
return request
}

async read(mbean: string): Promise<AttributeValues> {
return await jolokiaService.readAttributes(mbean)
return await jolokiaService.readAttributes(mbean, this.requestOptions())
}

async readWithCallback(mbean: string, callback: (attrs: AttributeValues) => void): Promise<void> {
const attrs = await jolokiaService.readAttributes(mbean)
const attrs = await jolokiaService.readAttributes(mbean, this.requestOptions())
callback(attrs)
}

async register(request: Request, callback: (response: Response) => void) {
const handle = await jolokiaService.register(request, callback)
const handle = await jolokiaService.register(this.setupConfig(request), callback)
log.debug('Register handle:', handle)
this.handles.push(handle)
}
Expand Down Expand Up @@ -47,12 +61,12 @@ class AttributeService {
}

async update(mbeanName: string, attribute: string, value: unknown) {
await jolokiaService.writeAttribute(mbeanName, attribute, value)
await jolokiaService.writeAttribute(mbeanName, attribute, value, this.requestOptions())
eventService.notify({ type: 'success', message: `Updated attribute: ${attribute}` })
}

async bulkRequest(requests: Request[]) {
return jolokiaService.bulkRequest(requests)
return jolokiaService.bulkRequest(requests.map(this.setupConfig))
}
}

Expand Down
16 changes: 16 additions & 0 deletions packages/hawtio/src/plugins/shared/jmx-preferences-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DEFAULT_OPTIONS, STORAGE_KEY_PREFERENCES, jmxPreferencesService } from './jmx-preferences-service'

describe('JmxPreferencesService', () => {
beforeEach(() => {
localStorage.removeItem(STORAGE_KEY_PREFERENCES)
})

test('loadOptions/saveOptions', () => {
let options = jmxPreferencesService.loadOptions()
expect(options).toEqual(DEFAULT_OPTIONS)

jmxPreferencesService.saveOptions({ serializeLong: true })
options = jmxPreferencesService.loadOptions()
expect(options).toEqual({ ...DEFAULT_OPTIONS, serializeLong: true })
})
})
29 changes: 29 additions & 0 deletions packages/hawtio/src/plugins/shared/jmx-preferences-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export const STORAGE_KEY_PREFERENCES = 'jmx.preferences'

export type JmxOptions = {
serializeLong: boolean
}

export const DEFAULT_OPTIONS: JmxOptions = {
serializeLong: false,
} as const

export interface IJmxPreferencesService {
loadOptions(): JmxOptions
saveOptions(options: Partial<JmxOptions>): void
}

class JmxPreferencesService implements IJmxPreferencesService {
loadOptions(): JmxOptions {
const item = localStorage.getItem(STORAGE_KEY_PREFERENCES)
const savedOptions = item ? JSON.parse(item) : {}
return { ...DEFAULT_OPTIONS, ...savedOptions }
}

saveOptions(options: Partial<JmxOptions>) {
const updated = { ...this.loadOptions(), ...options }
localStorage.setItem(STORAGE_KEY_PREFERENCES, JSON.stringify(updated))
}
}

export const jmxPreferencesService = new JmxPreferencesService()
54 changes: 39 additions & 15 deletions packages/hawtio/src/plugins/shared/jolokia-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import { parseBoolean } from '@hawtiosrc/util/strings'
import Jolokia, {
AttributeRequestOptions,
BaseRequestOptions,
BulkRequestOptions,
ErrorResponse,
ExecuteRequestOptions,
ListRequestOptions,
ListResponse,
NotificationMode,
Expand All @@ -33,8 +35,8 @@ import { func, is, object, type } from 'superstruct'
import {
PARAM_KEY_CONNECTION,
PARAM_KEY_REDIRECT,
connectService,
SESSION_KEY_CURRENT_CONNECTION,
connectService,
} from '../shared/connect-service'
import { log } from './globals'
import { OptimisedJmxDomains, OptimisedMBeanInfo, isJmxDomain, isJmxDomains, isMBeanInfo } from './tree'
Expand Down Expand Up @@ -111,11 +113,12 @@ export interface IJolokiaService {
getFullJolokiaUrl(): Promise<string>
list(options?: ListRequestOptions): Promise<OptimisedJmxDomains>
sublist(paths: string | string[], options?: ListRequestOptions): Promise<OptimisedJmxDomains>
readAttributes(mbean: string): Promise<AttributeValues>
readAttribute(mbean: string, attribute: string): Promise<unknown>
execute(mbean: string, operation: string, args?: unknown[]): Promise<unknown>
readAttributes(mbean: string, options?: RequestOptions): Promise<AttributeValues>
readAttribute(mbean: string, attribute: string, options?: RequestOptions): Promise<unknown>
writeAttribute(mbean: string, attribute: string, value: unknown, options?: RequestOptions): Promise<unknown>
execute(mbean: string, operation: string, args?: unknown[], options?: ExecuteRequestOptions): Promise<unknown>
search(mbeanPattern: string): Promise<string[]>
bulkRequest(requests: Request[]): Promise<Response[]>
bulkRequest(requests: Request[], options?: BulkRequestOptions): Promise<Response[]>
register(request: Request, callback: (response: Response | ErrorResponse) => void): Promise<number>
unregister(handle: number): void
loadUpdateRate(): number
Expand Down Expand Up @@ -651,7 +654,8 @@ class JolokiaService implements IJolokiaService {
requests,
onBulkSuccessAndError(
response => {
bulkResponse.push(response)
// Response can never be string in Hawtio's setup of Jolokia
bulkResponse.push(response as Response)
// Resolve only when all the responses from the bulk request are collected
if (bulkResponse.length === requests.length) {
mergeResponses()
Expand Down Expand Up @@ -686,55 +690,72 @@ class JolokiaService implements IJolokiaService {
return target
}

async readAttributes(mbean: string): Promise<AttributeValues> {
async readAttributes(mbean: string, options?: RequestOptions): Promise<AttributeValues> {
const jolokia = await this.getJolokia()
return new Promise(resolve => {
jolokia.request(
{ type: 'read', mbean },
onSuccessAndError(
response => resolve(response.value as AttributeValues),
response => {
// Response can never be string in Hawtio's setup of Jolokia
resolve((response as Response).value as AttributeValues)
},
error => {
log.error('Error during readAttributes:', error)
resolve({})
},
options,
),
)
})
}

async readAttribute(mbean: string, attribute: string): Promise<unknown> {
async readAttribute(mbean: string, attribute: string, options?: RequestOptions): Promise<unknown> {
const jolokia = await this.getJolokia()
return new Promise(resolve => {
jolokia.request(
{ type: 'read', mbean, attribute },
onSuccessAndError(
response => resolve(response.value as unknown),
response => {
// Response can never be string in Hawtio's setup of Jolokia
resolve((response as Response).value as unknown)
},
error => {
log.error('Error during readAttribute:', error)
resolve(null)
},
options,
),
)
})
}

async writeAttribute(mbean: string, attribute: string, value: unknown): Promise<unknown> {
async writeAttribute(mbean: string, attribute: string, value: unknown, options?: RequestOptions): Promise<unknown> {
const jolokia = await this.getJolokia()
return new Promise(resolve => {
jolokia.request(
{ type: 'write', mbean, attribute, value },
onSuccessAndError(
response => resolve(response.value as unknown),
response => {
// Response can never be string in Hawtio's setup of Jolokia
resolve((response as Response).value as unknown)
},
error => {
log.error('Error during writeAttribute:', error)
resolve(null)
},
options,
),
)
})
}

async execute(mbean: string, operation: string, args: unknown[] = []): Promise<unknown> {
async execute(
mbean: string,
operation: string,
args: unknown[] = [],
options?: ExecuteRequestOptions,
): Promise<unknown> {
const jolokia = await this.getJolokia()
return new Promise((resolve, reject) => {
jolokia.execute(
Expand All @@ -744,6 +765,7 @@ class JolokiaService implements IJolokiaService {
onExecuteSuccessAndError(
response => resolve(response),
error => reject(error.stacktrace || error.error),
options,
),
)
})
Expand All @@ -765,15 +787,16 @@ class JolokiaService implements IJolokiaService {
})
}

async bulkRequest(requests: Request[]): Promise<Response[]> {
async bulkRequest(requests: Request[], options?: BulkRequestOptions): Promise<Response[]> {
const jolokia = await this.getJolokia()
return new Promise(resolve => {
const bulkResponse: Response[] = []
jolokia.request(
requests,
onBulkSuccessAndError(
response => {
bulkResponse.push(response)
// Response can never be string in Hawtio's setup of Jolokia
bulkResponse.push(response as Response)
// Resolve only when all the responses from the bulk request are collected
if (bulkResponse.length === requests.length) {
resolve(bulkResponse)
Expand All @@ -787,6 +810,7 @@ class JolokiaService implements IJolokiaService {
resolve(bulkResponse)
}
},
options,
),
)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { jolokiaService } from '@hawtiosrc/plugins/shared/jolokia-service'
import { escapeMBean } from '@hawtiosrc/util/jolokia'
import { ExecuteRequestOptions } from 'jolokia.js'
import { log } from '../globals'
import { jmxPreferencesService } from '../jmx-preferences-service'

class OperationService {
private requestOptions(): ExecuteRequestOptions {
const { serializeLong } = jmxPreferencesService.loadOptions()
return serializeLong ? { serializeLong: 'string' } : {}
}

async execute(mbean: string, operation: string, args: unknown[]): Promise<unknown> {
log.debug('Execute:', mbean, '-', operation, '-', args)
return jolokiaService.execute(mbean, operation, args)
return jolokiaService.execute(mbean, operation, args, this.requestOptions())
}

async getJolokiaUrl(mbean: string, operation: string): Promise<string> {
Expand Down
Loading

0 comments on commit 83c71a1

Please sign in to comment.