Output runtime diagnostics data as JSON, XML or PlantUML diagrams

Understanding a system’s runtime state can be challenging. Monitoring endpoints and logs often provide data that is difficult to interpret. When analyzing the internal behavior of a system, dedicated diagnostic information can be invaluable, especially when visualized. Generating on-demand snapshots of a system’s state can reveal what is happening under the hood. Once canonical schemas that represent system state are in place, generating diagnostics and reports in XML, JSON or PlantUML becomes straightforward.

Diagnostic interfaces, beyond the well-known monitoring endpoints, should be treated as first-class citizens of critical deployment artifact. In production environments, you cannot simply place breakpoints or inspect variables with your IDE to understand a system’s internal state. Without dedicated diagnostic interfaces that expose runtime information, reproducing issues requires a debug-enabled environment with an attached debugger, which is rarely feasible in production. Moreover, some issues only become apparent and reproducible only when you have knowledge of the system’s internal state. A common but impractical workaround is to deploy versions filled with additional logging statements tracing variable values, which clutters both the logs and the codebase. A more effective approach is to provide a diagnostic interface that can generate reports and snapshots of the system’s internal state on demand.

The diagnostic interface may be a web endpoint that accepts query parameters to isolate the data of interest and its representation or an MBean accessible via JMX through tools such as JConsole. Given the broad support of web endpoints by the various motoring frameworks, web endpoints are considered a natural choice for a diagnostic interface.

JSON alike document for a serial data communication buffer at runtime […]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
CrcSegmentDecorator: {
  ALIAS: "CrcSegmentDecorator",
  CRC_ALGORITHM: "CRC-16/CCITT-FALSE",
  CRC_BYTE_WIDTH: 2,
  CRC_CHECKSUM: 22530,
  CRC_CHECKSUM_CONCATENATION_MODE: "PREPEND",
  CRC_CHECKSUM_HEX: { 0x02, 0x58 },
  CRC_CHECKSUM_LITTLE_ENDIAN_BYTES: { 0x02, 0x58 },
  CRC_ENDIANESS: "LITTLE",
  DESCRIPTION: "A segment decorator enriching the encapsulated segment with a CRC checksum.",
  HASH: 1344645519,
  IDENTIFIER: "CrcSegmentDecorator@5025a98f",
  LENGTH: 21,
  TYPE: "org.refcodes.serial.CrcSegmentDecorator",
  VALUE: { 0x02, 0x58, 0x01, 0x29, 0x14, 0x00, 0x00, 0x0c, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21 },
  SegmentComposite: {
    ALIAS: "SegmentComposite",
    DESCRIPTION: "A body containing a composite segment as payload.",
    HASH: 1414521932,
    IDENTIFIER: "SegmentComposite@544fe44c",
    LENGTH: 19,
    TYPE: "org.refcodes.serial.SegmentComposite",
    VALUE: { 0x01, 0x29, 0x14, 0x00, 0x00, 0x0c, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21 },
    BooleanSegment: {
      ALIAS: "booleanSegment",
      DESCRIPTION: "A segment containing an boolean payload.",
      HASH: 101478235,
      IDENTIFIER: "BooleanSegment@60c6f5b",
      LENGTH: 1,
      TYPE: "org.refcodes.serial.BooleanSegment",
      VALUE: { 0x01 },
      VERBOSE: "true"
    },
    IntSegment: {
      ALIAS: "intSegment",
      DESCRIPTION: "A body containing an integer payload.",
      ENDIANESS: "LITTLE",
      HASH: 540585569,
      IDENTIFIER: "IntSegment@2038ae61",
      LENGTH: 4,
      TYPE: "org.refcodes.serial.IntSegment",
      VALUE: { 0x29, 0x14, 0x00, 0x00 },
      VERBOSE: "5161"
    },
    AllocSectionDecoratorSegment: {
      ALIAS: "AllocSectionDecoratorSegment",
      ALLOC_LENGTH: 12,
      ALLOC_LENGTH_WIDTH: 2,
      DESCRIPTION: "An allocation decorator referencing a decoratee and prefixing the length of the decoratee in bytes.",
      ENDIANESS: "LITTLE",
      HASH: 1007653873,
      IDENTIFIER: "AllocSectionDecoratorSegment@3c0f93f1",
      LENGTH: 14,
      TYPE: "org.refcodes.serial.AllocSectionDecoratorSegment",
      VALUE: { 0x0c, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21 },
      StringSection: {
        ALIAS: "stringSection",
        DESCRIPTION: "A section containing a string payload.",
        HASH: 836514715,
        IDENTIFIER: "StringSection@31dc339b",
        LENGTH: 12,
        TYPE: "org.refcodes.serial.StringSection",
        VALUE: { 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21 },
        VERBOSE: "Hello world!"
      }
    }
  }
}

The diagnostics data may be served in an arbitrary notation which then is to be interpreted as required. For example, the data may be provided as XML or JSON and then further processed or as PlantUML for visualization and analysis.

Providing a diagnostic interface involves three key parts: First, gather the runtime state in a canonical schema. Second, generate reports from that schema in formats such as XML, JSON, or PlantUML. Third, expose these generated reports through a diagnostic interface for easy access and analysis.

1. Gather the runtime state in a canonical schema

The canonical model captures the system’s runtime state. This diagnostic data is organized as nested instances of a Schema class. To remain flexible, the data is stored in a map of arbitrary key/value pairs. For convenience, common diagnostic properties such as identifier, alias, value, or description can be exposed through dedicated methods with the meaning of these properties depending on the specific diagnostic purpose.

Any data structure intended to provide diagnostic data implements the Schemable interface, which requires the implementation of a toSchema() method. This method returns a Schema instance that describes the diagnostic data of the structure. If any member fields within the object graph also implement Schemable, their toSchema() methods are invoked in turn, and their resulting Schema instances are added as children to the current one. This recursive process builds a nested Schema hierarchy that represents the complete diagnostic view of the data structure.

2. Generate XML, JSON, or PlantUML reports from the schema

The SchemaVisitor interface defines methods for traversing a Schema hierarchy according to the Visitor pattern. By invoking a Schema’s visit() method with a specific SchemaVisitor implementation, reports can be generated in various formats. The visitor traverses the entire Schema hierarchy and, depending on its implementation, produces output in JSON, XML, PlantUML, or other notations.

PlantUML source code of a serial data communication buffer at runtime […]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
@startuml

object "ComplexTypeSegment@37574691" {
  ALIAS = "ComplexTypeSegment"
  DESCRIPTION = "A body containing a composite segment as payload."
  HASH = 928466577
  IDENTIFIER = "ComplexTypeSegment@37574691"
  LENGTH = 53
  TYPE = org.refcodes.serial.ComplexTypeSegment
  VALUE = [0x33, 0x00, 0xaf, 0x93, 0x01, 0x00, 0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x41, 0xaf, 0x93, 0x01, 0x00, 0x5d, 0x56, 0x00, 0x00, 0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x42, 0x5d, 0x56, 0x00, 0x00, 0xad, 0xc9, 0x04, 0x00, 0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x43, 0xad, 0xc9, 0x04, 0x00]
}

object "AllocSectionDecoratorSegment@70beb599" {
  ALIAS = "AllocSectionDecoratorSegment"
  ALLOC_LENGTH = 51
  ALLOC_LENGTH_WIDTH = 2
  DESCRIPTION = "An allocation decorator referencing a decoratee and prefixing the length of the decoratee in bytes."
  ENDIANESS = "LITTLE"
  HASH = 1891546521
  IDENTIFIER = "AllocSectionDecoratorSegment@70beb599"
  LENGTH = 53
  TYPE = org.refcodes.serial.AllocSectionDecoratorSegment
  VALUE = [0x33, 0x00, 0xaf, 0x93, 0x01, 0x00, 0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x41, 0xaf, 0x93, 0x01, 0x00, 0x5d, 0x56, 0x00, 0x00, 0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x42, 0x5d, 0x56, 0x00, 0x00, 0xad, 0xc9, 0x04, 0x00, 0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x43, 0xad, 0xc9, 0x04, 0x00]
}

object "SegmentArraySection@6a79c292" {
  ALIAS = "/"
  DESCRIPTION = "An array segment containing a fixed length elements array as payload."
  HASH = 1786364562
  IDENTIFIER = "SegmentArraySection@6a79c292"
  LENGTH = 51
  TYPE = org.refcodes.serial.SegmentArraySection
  VALUE = [0xaf, 0x93, 0x01, 0x00, 0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x41, 0xaf, 0x93, 0x01, 0x00, 0x5d, 0x56, 0x00, 0x00, 0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x42, 0x5d, 0x56, 0x00, 0x00, 0xad, 0xc9, 0x04, 0x00, 0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x43, 0xad, 0xc9, 0x04, 0x00]
}

object "SegmentComposite@11e21d0e" {
  ALIAS = "SegmentComposite"
  DESCRIPTION = "A body containing a composite segment as payload."
  HASH = 300031246
  IDENTIFIER = "SegmentComposite@11e21d0e"
  LENGTH = 17
  TYPE = org.refcodes.serial.SegmentComposite
  VALUE = [0xaf, 0x93, 0x01, 0x00, 0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x41, 0xaf, 0x93, 0x01, 0x00]
}

object "IntSegment@5e25a92e" {
  ALIAS = "/payload"
  DESCRIPTION = "A body containing an integer payload."
  ENDIANESS = "LITTLE"
  HASH = 1579526446
  IDENTIFIER = "IntSegment@5e25a92e"
  LENGTH = 4
  TYPE = org.refcodes.serial.IntSegment
  VALUE = [0xaf, 0x93, 0x01, 0x00]
  VERBOSE = "103343"
}

object "AllocSectionDecoratorSegment@4df828d7" {
  ALIAS = "AllocSectionDecoratorSegment"
  ALLOC_LENGTH = 7
  ALLOC_LENGTH_WIDTH = 2
  DESCRIPTION = "An allocation decorator referencing a decoratee and prefixing the length of the decoratee in bytes."
  ENDIANESS = "LITTLE"
  HASH = 1308109015
  IDENTIFIER = "AllocSectionDecoratorSegment@4df828d7"
  LENGTH = 9
  TYPE = org.refcodes.serial.AllocSectionDecoratorSegment
  VALUE = [0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x41]
}

object "StringSection@b59d31" {
  ALIAS = "/name"
  DESCRIPTION = "A section containing a string payload."
  HASH = 11902257
  IDENTIFIER = "StringSection@b59d31"
  LENGTH = 7
  TYPE = org.refcodes.serial.StringSection
  VALUE = [0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x41]
  VERBOSE = "SensorA"
}

"AllocSectionDecoratorSegment@4df828d7" --> "StringSection@b59d31" : <has>

object "IntSegment@62fdb4a6" {
  ALIAS = "/value"
  DESCRIPTION = "A body containing an integer payload."
  ENDIANESS = "LITTLE"
  HASH = 1660794022
  IDENTIFIER = "IntSegment@62fdb4a6"
  LENGTH = 4
  TYPE = org.refcodes.serial.IntSegment
  VALUE = [0xaf, 0x93, 0x01, 0x00]
  VERBOSE = "103343"
}

"SegmentComposite@11e21d0e" --> "IntSegment@5e25a92e" : <has>
"SegmentComposite@11e21d0e" --> "AllocSectionDecoratorSegment@4df828d7" : <has>
"SegmentComposite@11e21d0e" --> "IntSegment@62fdb4a6" : <has>

object "SegmentComposite@23bb8443" {
  ALIAS = "SegmentComposite"
  DESCRIPTION = "A body containing a composite segment as payload."
  HASH = 599491651
  IDENTIFIER = "SegmentComposite@23bb8443"
  LENGTH = 17
  TYPE = org.refcodes.serial.SegmentComposite
  VALUE = [0x5d, 0x56, 0x00, 0x00, 0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x42, 0x5d, 0x56, 0x00, 0x00]
}

object "IntSegment@1dd02175" {
  ALIAS = "/payload"
  DESCRIPTION = "A body containing an integer payload."
  ENDIANESS = "LITTLE"
  HASH = 500179317
  IDENTIFIER = "IntSegment@1dd02175"
  LENGTH = 4
  TYPE = org.refcodes.serial.IntSegment
  VALUE = [0x5d, 0x56, 0x00, 0x00]
  VERBOSE = "22109"
}

object "AllocSectionDecoratorSegment@31206beb" {
  ALIAS = "AllocSectionDecoratorSegment"
  ALLOC_LENGTH = 7
  ALLOC_LENGTH_WIDTH = 2
  DESCRIPTION = "An allocation decorator referencing a decoratee and prefixing the length of the decoratee in bytes."
  ENDIANESS = "LITTLE"
  HASH = 824208363
  IDENTIFIER = "AllocSectionDecoratorSegment@31206beb"
  LENGTH = 9
  TYPE = org.refcodes.serial.AllocSectionDecoratorSegment
  VALUE = [0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x42]
}

object "StringSection@3e77a1ed" {
  ALIAS = "/name"
  DESCRIPTION = "A section containing a string payload."
  HASH = 1048027629
  IDENTIFIER = "StringSection@3e77a1ed"
  LENGTH = 7
  TYPE = org.refcodes.serial.StringSection
  VALUE = [0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x42]
  VERBOSE = "SensorB"
}

"AllocSectionDecoratorSegment@31206beb" --> "StringSection@3e77a1ed" : <has>

object "IntSegment@3ffcd140" {
  ALIAS = "/value"
  DESCRIPTION = "A body containing an integer payload."
  ENDIANESS = "LITTLE"
  HASH = 1073533248
  IDENTIFIER = "IntSegment@3ffcd140"
  LENGTH = 4
  TYPE = org.refcodes.serial.IntSegment
  VALUE = [0x5d, 0x56, 0x00, 0x00]
  VERBOSE = "22109"
}

"SegmentComposite@23bb8443" --> "IntSegment@1dd02175" : <has>
"SegmentComposite@23bb8443" --> "AllocSectionDecoratorSegment@31206beb" : <has>
"SegmentComposite@23bb8443" --> "IntSegment@3ffcd140" : <has>

object "SegmentComposite@1372ed45" {
  ALIAS = "SegmentComposite"
  DESCRIPTION = "A body containing a composite segment as payload."
  HASH = 326298949
  IDENTIFIER = "SegmentComposite@1372ed45"
  LENGTH = 17
  TYPE = org.refcodes.serial.SegmentComposite
  VALUE = [0xad, 0xc9, 0x04, 0x00, 0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x43, 0xad, 0xc9, 0x04, 0x00]
}

object "IntSegment@1176dcec" {
  ALIAS = "/payload"
  DESCRIPTION = "A body containing an integer payload."
  ENDIANESS = "LITTLE"
  HASH = 293002476
  IDENTIFIER = "IntSegment@1176dcec"
  LENGTH = 4
  TYPE = org.refcodes.serial.IntSegment
  VALUE = [0xad, 0xc9, 0x04, 0x00]
  VERBOSE = "313773"
}

object "AllocSectionDecoratorSegment@120d6fe6" {
  ALIAS = "AllocSectionDecoratorSegment"
  ALLOC_LENGTH = 7
  ALLOC_LENGTH_WIDTH = 2
  DESCRIPTION = "An allocation decorator referencing a decoratee and prefixing the length of the decoratee in bytes."
  ENDIANESS = "LITTLE"
  HASH = 302870502
  IDENTIFIER = "AllocSectionDecoratorSegment@120d6fe6"
  LENGTH = 9
  TYPE = org.refcodes.serial.AllocSectionDecoratorSegment
  VALUE = [0x07, 0x00, 0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x43]
}

object "StringSection@4ba2ca36" {
  ALIAS = "/name"
  DESCRIPTION = "A section containing a string payload."
  HASH = 1268959798
  IDENTIFIER = "StringSection@4ba2ca36"
  LENGTH = 7
  TYPE = org.refcodes.serial.StringSection
  VALUE = [0x53, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x43]
  VERBOSE = "SensorC"
}

"AllocSectionDecoratorSegment@120d6fe6" --> "StringSection@4ba2ca36" : <has>

object "IntSegment@3444d69d" {
  ALIAS = "/value"
  DESCRIPTION = "A body containing an integer payload."
  ENDIANESS = "LITTLE"
  HASH = 876926621
  IDENTIFIER = "IntSegment@3444d69d"
  LENGTH = 4
  TYPE = org.refcodes.serial.IntSegment
  VALUE = [0xad, 0xc9, 0x04, 0x00]
  VERBOSE = "313773"
}

"SegmentComposite@1372ed45" --> "IntSegment@1176dcec" : <has>
"SegmentComposite@1372ed45" --> "AllocSectionDecoratorSegment@120d6fe6" : <has>
"SegmentComposite@1372ed45" --> "IntSegment@3444d69d" : <has>

"SegmentArraySection@6a79c292" --> "SegmentComposite@11e21d0e" : <has>
"SegmentArraySection@6a79c292" --> "SegmentComposite@23bb8443" : <has>
"SegmentArraySection@6a79c292" --> "SegmentComposite@1372ed45" : <has>

"AllocSectionDecoratorSegment@70beb599" --> "SegmentArraySection@6a79c292" : <has>

"ComplexTypeSegment@37574691" --> "AllocSectionDecoratorSegment@70beb599" : <has>
@enduml

These reports can then be served through a diagnostic interface for easy access and analysis. In case of a PlantUmlVisitor implementation, the result will be a a text in PlantUML notation which then can be rendered as UML diagrams.

Serial data diagnostics

3. Expose the generated reports through a diagnostic interface

Using Spring Boot, the generated reports can be served through a WebEndpoint that implements the ReadOperation method. The example below shows how this can be done.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@WebEndpoint(id = "diagnostics")
public class DiagnosticsEndpoint {

	private Schemable data = ... // The data variable holds an instance implementing the Schemable interface

	// ...

	@ReadOperation
	public WebEndpointResponse<String> getDiagnosticsAsPlantUml() {
	    Schema schema = data.toSchema();
	    String diagnostics = schema.visit(new PlantUmlVisitor());
	    return new WebEndpointResponse<>(diagnostics, 200, "text/x-plantuml");
	}
	
	// ...

}

Conclusion

Diagnostic interfaces bridge the gap between static monitoring and true runtime understanding. By exposing canonical diagnostic schemas through standardized interfaces, systems can provide meaningful insights without invasive debugging or excessive logging. The approach described here enables developers and operators to visualize complex runtime structures as UML diagrams, or to process the same data in machine-readable formats such as JSON or XML.

With a consistent schema model and visitor based report generation, runtime introspection becomes both systematic and extensible. Whether integrated into a Spring Boot endpoint or accessed via JMX, such diagnostic interfaces turn opaque runtime behavior into accessible, analyzable knowledge, helping to maintain stability, trace issues faster, and understand what really happens under the hood.