From 1492b68c5ba9bfb7a2b23fa18bd97999369a478a Mon Sep 17 00:00:00 2001 From: Norbert Hartl Date: Thu, 1 Oct 2026 10:01:35 +0200 Subject: [PATCH] Let the node and the value holder carry a description A branch, a cell and an accessor now answer description, and the value holder carries description and labels instead of only CanopyMetric. The HELP line takes the node's description first, then the holder's, then the key. type and readingsDo: stay with CanopyMetric, so the export filter is unchanged. Co-Authored-By: Claude Sonnet 5.5 --- .../CanopyDescriptionTest.class.st | 35 +++++++++++++++++++ source/Canopy-Core/CanopyNode.class.st | 16 ++++++++- source/Canopy-Core/CanopyValueHolder.class.st | 34 +++++++++++++++++- .../CanopyDeclaredMetricTest.class.st | 14 ++++++++ source/Canopy-Metrics/CanopyMetric.class.st | 32 +---------------- .../CanopyPrometheusVisitor.class.st | 3 +- 6 files changed, 100 insertions(+), 34 deletions(-) create mode 100644 source/Canopy-Core-Tests/CanopyDescriptionTest.class.st diff --git a/source/Canopy-Core-Tests/CanopyDescriptionTest.class.st b/source/Canopy-Core-Tests/CanopyDescriptionTest.class.st new file mode 100644 index 0000000..87e3b51 --- /dev/null +++ b/source/Canopy-Core-Tests/CanopyDescriptionTest.class.st @@ -0,0 +1,35 @@ +Class { + #name : 'CanopyDescriptionTest', + #superclass : 'TestCase', + #category : 'Canopy-Core-Tests', + #package : 'Canopy-Core-Tests' +} + +{ #category : 'tests' } +CanopyDescriptionTest >> testABranchAndACellCarryADescription [ + | branch cell | + branch := CanopyBranchNode new. + cell := CanopyCell new value: 1. + branch description: 'where the limits live'. + cell description: 'the maximum of open files'. + self assert: branch description equals: 'where the limits live'. + self assert: cell description equals: 'the maximum of open files' +] + +{ #category : 'tests' } +CanopyDescriptionTest >> testAHolderCarriesADescriptionAndLabels [ + | holder | + holder := CanopyValue value: 1. + holder description: 'what the component says'. + holder labelAt: #database put: 'alpha'. + self assert: holder description equals: 'what the component says'. + self assert: (holder labels at: #database) equals: 'alpha' +] + +{ #category : 'tests' } +CanopyDescriptionTest >> testNothingIsDescribedUnlessSomeoneSaidSo [ + self assert: CanopyBranchNode new description isNil. + self assert: (CanopyCell new value: 1) description isNil. + self assert: (CanopyValue value: 1) description isNil. + self assert: (CanopyValue value: 1) labels isEmpty +] diff --git a/source/Canopy-Core/CanopyNode.class.st b/source/Canopy-Core/CanopyNode.class.st index b3a9e22..eff130e 100644 --- a/source/Canopy-Core/CanopyNode.class.st +++ b/source/Canopy-Core/CanopyNode.class.st @@ -4,7 +4,8 @@ Class { #instVars : [ 'parent', 'tags', - 'labels' + 'labels', + 'description' ], #category : 'Canopy-Core', #package : 'Canopy-Core' @@ -28,6 +29,19 @@ CanopyNode >> allKeys [ ifNil: [ #() ] ] +{ #category : 'accessing' } +CanopyNode >> description [ + "What this node is for, declared where the tree is built. Branches, cells and accessors all + carry one. Nil when nobody said; the export then falls back to what the value holder of a + leaf brings along, and last to the key." + ^ description +] + +{ #category : 'accessing' } +CanopyNode >> description: aString [ + description := aString +] + { #category : 'accessing' } CanopyNode >> fullKey [ ^ '_' join: (self allKeys collect: [ :k | k asString ]) diff --git a/source/Canopy-Core/CanopyValueHolder.class.st b/source/Canopy-Core/CanopyValueHolder.class.st index 420eab0..da86a2a 100644 --- a/source/Canopy-Core/CanopyValueHolder.class.st +++ b/source/Canopy-Core/CanopyValueHolder.class.st @@ -2,7 +2,9 @@ Class { #name : 'CanopyValueHolder', #superclass : 'Object', #instVars : [ - 'value' + 'value', + 'description', + 'labels' ], #category : 'Canopy-Core', #package : 'Canopy-Core' @@ -22,6 +24,36 @@ CanopyValueHolder >> asCanopyValue [ ^ self ] +{ #category : 'accessing' } +CanopyValueHolder >> description [ + "What the component itself says this value is for. A description on the node wins over + this one - see CanopyNode>>description - so this is the fallback a component brings along." + ^ description +] + +{ #category : 'accessing' } +CanopyValueHolder >> description: aString [ + description := aString +] + +{ #category : 'initialization' } +CanopyValueHolder >> initialize [ + super initialize. + labels := Dictionary new +] + +{ #category : 'accessing' } +CanopyValueHolder >> labelAt: aKey put: anObject [ + "A label the component itself knows, as opposed to one the tree carries - the stage of a + deployment hangs at the root, the database a number belongs to is known here." + labels at: aKey put: anObject +] + +{ #category : 'accessing' } +CanopyValueHolder >> labels [ + ^ labels +] + { #category : 'printing' } CanopyValueHolder >> printOn: aStream [ super printOn: aStream. diff --git a/source/Canopy-Metrics-Tests/CanopyDeclaredMetricTest.class.st b/source/Canopy-Metrics-Tests/CanopyDeclaredMetricTest.class.st index 1e3cfe3..40dbd24 100644 --- a/source/Canopy-Metrics-Tests/CanopyDeclaredMetricTest.class.st +++ b/source/Canopy-Metrics-Tests/CanopyDeclaredMetricTest.class.st @@ -25,6 +25,20 @@ CanopyDeclaredMetricTest >> setUp [ branch := root /+ #server ] +{ #category : 'tests' } +CanopyDeclaredMetricTest >> testTheNodeDescriptionBeatsTheOneOfTheHolder [ + "The node is the declaration made where the tree is built, the holder only what the component + brings along when it is read - so the node wins, and the holder answers only where the node + says nothing." + | described | + branch counter: #requests description: 'from the holder' reading: [ 3 ]. + branch counter: #errors description: 'from the holder' reading: [ 1 ]. + described := branch / #requests. + described description: 'from the node'. + self assert: (self scrape includesSubstring: '# HELP server_requests from the node'). + self assert: (self scrape includesSubstring: '# HELP server_errors from the holder') +] + { #category : 'tests' } CanopyDeclaredMetricTest >> testACounterIsWrittenAsOne [ branch counter: #requests description: 'requests served' reading: [ 3 ]. diff --git a/source/Canopy-Metrics/CanopyMetric.class.st b/source/Canopy-Metrics/CanopyMetric.class.st index c15e6dc..dc5de97 100644 --- a/source/Canopy-Metrics/CanopyMetric.class.st +++ b/source/Canopy-Metrics/CanopyMetric.class.st @@ -2,42 +2,12 @@ Class { #name : 'CanopyMetric', #superclass : 'CanopyValueHolder', #instVars : [ - 'type', - 'description', - 'labels' + 'type' ], #category : 'Canopy-Metrics', #package : 'Canopy-Metrics' } -{ #category : 'accessing' } -CanopyMetric >> description [ - ^ description -] - -{ #category : 'accessing' } -CanopyMetric >> description: aString [ - description := aString -] - -{ #category : 'initialization' } -CanopyMetric >> initialize [ - super initialize. - labels := Dictionary new -] - -{ #category : 'accessing' } -CanopyMetric >> labelAt: aKey put: anObject [ - "A label the component itself knows, as opposed to one the tree carries - the stage of a - deployment hangs at the root, the database a number belongs to is known here." - labels at: aKey put: anObject -] - -{ #category : 'accessing' } -CanopyMetric >> labels [ - ^ labels -] - { #category : 'enumerating' } CanopyMetric >> readingsDo: aBlock [ "One reading: the labels this metric knows itself, and its value. Whatever the tree adds is diff --git a/source/Canopy-Metrics/CanopyPrometheusVisitor.class.st b/source/Canopy-Metrics/CanopyPrometheusVisitor.class.st index 2dfc3d4..d19981d 100644 --- a/source/Canopy-Metrics/CanopyPrometheusVisitor.class.st +++ b/source/Canopy-Metrics/CanopyPrometheusVisitor.class.st @@ -112,7 +112,8 @@ CanopyPrometheusVisitor >> writeHelpLine: aNode holder: aMetric [ nextPutAll: '# HELP '; nextPutAll: (self convertMetricName: aNode); nextPutAll: ' '. - stream nextPutAll: (aMetric description ifNil: [ aNode key asString ]). + stream nextPutAll: (aNode description + ifNil: [ aMetric description ifNil: [ aNode key asString ] ]). stream nextPut: Character linefeed ]