Skip to content

Commit 4d662cf

Browse files
authored
fix(sdk-metrics): metric names should be case-insensitive (#4059)
* fix(sdk-metrics): metric names should be case-insensitive * fixup! * fixup!
1 parent 9452607 commit 4d662cf

File tree

6 files changed

+173
-17
lines changed

6 files changed

+173
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/
1818
### :bug: (Bug Fix)
1919

2020
* fix(exporter-zipkin): rounding duration to the nearest int to be compliant with zipkin protocol [#4064](https://github.com/open-telemetry/opentelemetry-js/pull/4064) @n0cloud
21+
* fix(sdk-metrics): metric names should be case-insensitive
2122

2223
### :books: (Refine Doc)
2324

packages/sdk-metrics/src/InstrumentDescriptor.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { MetricOptions, ValueType } from '@opentelemetry/api';
17+
import { MetricOptions, ValueType, diag } from '@opentelemetry/api';
1818
import { View } from './view/View';
19+
import { equalsCaseInsensitive } from './utils';
1920

2021
/**
2122
* Supported types of metric instruments.
@@ -45,6 +46,11 @@ export function createInstrumentDescriptor(
4546
type: InstrumentType,
4647
options?: MetricOptions
4748
): InstrumentDescriptor {
49+
if (!isValidName(name)) {
50+
diag.warn(
51+
`Invalid metric name: "${name}". The metric name should be a ASCII string with a length no greater than 255 characters.`
52+
);
53+
}
4854
return {
4955
name,
5056
type,
@@ -71,10 +77,18 @@ export function isDescriptorCompatibleWith(
7177
descriptor: InstrumentDescriptor,
7278
otherDescriptor: InstrumentDescriptor
7379
) {
80+
// Names are case-insensitive strings.
7481
return (
75-
descriptor.name === otherDescriptor.name &&
82+
equalsCaseInsensitive(descriptor.name, otherDescriptor.name) &&
7683
descriptor.unit === otherDescriptor.unit &&
7784
descriptor.type === otherDescriptor.type &&
7885
descriptor.valueType === otherDescriptor.valueType
7986
);
8087
}
88+
89+
// ASCII string with a length no greater than 255 characters.
90+
// NB: the first character counted separately from the rest.
91+
const NAME_REGEXP = /^[a-z][a-z0-9_.-]{0,254}$/i;
92+
export function isValidName(name: string): boolean {
93+
return name.match(NAME_REGEXP) != null;
94+
}

packages/sdk-metrics/src/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,7 @@ export function binarySearchLB(arr: number[], value: number): number {
190190
}
191191
return -1;
192192
}
193+
194+
export function equalsCaseInsensitive(lhs: string, rhs: string): boolean {
195+
return lhs.toLowerCase() === rhs.toLowerCase();
196+
}

packages/sdk-metrics/test/InstrumentDescriptor.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@
1717
import * as assert from 'assert';
1818
import {
1919
createInstrumentDescriptor,
20+
InstrumentDescriptor,
2021
InstrumentType,
22+
isValidName,
23+
isDescriptorCompatibleWith,
2124
} from '../src/InstrumentDescriptor';
25+
import { invalidNames, validNames } from './util';
26+
import { ValueType } from '@opentelemetry/api';
2227

2328
describe('InstrumentDescriptor', () => {
2429
describe('createInstrumentDescriptor', () => {
@@ -35,4 +40,105 @@ describe('InstrumentDescriptor', () => {
3540
});
3641
}
3742
});
43+
44+
describe('isDescriptorCompatibleWith', () => {
45+
const tests: [
46+
string,
47+
boolean,
48+
InstrumentDescriptor,
49+
InstrumentDescriptor,
50+
][] = [
51+
[
52+
'Compatible with identical descriptors',
53+
true,
54+
{
55+
name: 'foo',
56+
description: 'any descriptions',
57+
unit: 'kB',
58+
type: InstrumentType.COUNTER,
59+
valueType: ValueType.DOUBLE,
60+
},
61+
{
62+
name: 'foo',
63+
description: 'any descriptions',
64+
unit: 'kB',
65+
type: InstrumentType.COUNTER,
66+
valueType: ValueType.DOUBLE,
67+
},
68+
],
69+
[
70+
'Compatible with case-insensitive names',
71+
true,
72+
{
73+
name: 'foo',
74+
description: '',
75+
unit: '',
76+
type: InstrumentType.COUNTER,
77+
valueType: ValueType.DOUBLE,
78+
},
79+
{
80+
name: 'FoO',
81+
description: '',
82+
unit: '',
83+
type: InstrumentType.COUNTER,
84+
valueType: ValueType.DOUBLE,
85+
},
86+
],
87+
[
88+
'Incompatible with different names',
89+
false,
90+
{
91+
name: 'foo',
92+
description: '',
93+
unit: '',
94+
type: InstrumentType.COUNTER,
95+
valueType: ValueType.DOUBLE,
96+
},
97+
{
98+
name: 'foobar',
99+
description: '',
100+
unit: '',
101+
type: InstrumentType.COUNTER,
102+
valueType: ValueType.DOUBLE,
103+
},
104+
],
105+
[
106+
'Incompatible with case-sensitive units',
107+
false,
108+
{
109+
name: 'foo',
110+
description: '',
111+
unit: 'kB',
112+
type: InstrumentType.COUNTER,
113+
valueType: ValueType.DOUBLE,
114+
},
115+
{
116+
name: 'foo',
117+
description: '',
118+
unit: 'kb',
119+
type: InstrumentType.COUNTER,
120+
valueType: ValueType.DOUBLE,
121+
},
122+
],
123+
];
124+
for (const test of tests) {
125+
it(test[0], () => {
126+
assert.ok(isDescriptorCompatibleWith(test[2], test[3]) === test[1]);
127+
});
128+
}
129+
});
130+
131+
describe('isValidName', () => {
132+
it('should validate names', () => {
133+
for (const invalidName of invalidNames) {
134+
assert.ok(
135+
!isValidName(invalidName),
136+
`${invalidName} should be invalid`
137+
);
138+
}
139+
for (const validName of validNames) {
140+
assert.ok(isValidName(validName), `${validName} should be valid`);
141+
}
142+
});
143+
});
38144
});

packages/sdk-metrics/test/Meter.test.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Observable } from '@opentelemetry/api';
17+
import { Observable, diag } from '@opentelemetry/api';
1818
import * as assert from 'assert';
19+
import * as sinon from 'sinon';
1920
import {
2021
CounterInstrument,
2122
HistogramInstrument,
@@ -27,78 +28,86 @@ import {
2728
import { Meter } from '../src/Meter';
2829
import { MeterProviderSharedState } from '../src/state/MeterProviderSharedState';
2930
import { MeterSharedState } from '../src/state/MeterSharedState';
30-
import { defaultInstrumentationScope, defaultResource } from './util';
31+
import {
32+
defaultInstrumentationScope,
33+
defaultResource,
34+
invalidNames,
35+
validNames,
36+
} from './util';
3137

3238
describe('Meter', () => {
39+
afterEach(() => {
40+
sinon.restore();
41+
});
42+
3343
describe('createCounter', () => {
34-
it('should create counter', () => {
44+
testWithNames('counter', name => {
3545
const meterSharedState = new MeterSharedState(
3646
new MeterProviderSharedState(defaultResource),
3747
defaultInstrumentationScope
3848
);
3949
const meter = new Meter(meterSharedState);
40-
const counter = meter.createCounter('foobar');
50+
const counter = meter.createCounter(name);
4151
assert(counter instanceof CounterInstrument);
4252
});
4353
});
4454

4555
describe('createUpDownCounter', () => {
46-
it('should create up down counter', () => {
56+
testWithNames('UpDownCounter', name => {
4757
const meterSharedState = new MeterSharedState(
4858
new MeterProviderSharedState(defaultResource),
4959
defaultInstrumentationScope
5060
);
5161
const meter = new Meter(meterSharedState);
52-
const upDownCounter = meter.createUpDownCounter('foobar');
62+
const upDownCounter = meter.createUpDownCounter(name);
5363
assert(upDownCounter instanceof UpDownCounterInstrument);
5464
});
5565
});
5666

5767
describe('createHistogram', () => {
58-
it('should create histogram', () => {
68+
testWithNames('Histogram', name => {
5969
const meterSharedState = new MeterSharedState(
6070
new MeterProviderSharedState(defaultResource),
6171
defaultInstrumentationScope
6272
);
6373
const meter = new Meter(meterSharedState);
64-
const histogram = meter.createHistogram('foobar');
74+
const histogram = meter.createHistogram(name);
6575
assert(histogram instanceof HistogramInstrument);
6676
});
6777
});
6878

6979
describe('createObservableGauge', () => {
70-
it('should create observable gauge', () => {
80+
testWithNames('ObservableGauge', name => {
7181
const meterSharedState = new MeterSharedState(
7282
new MeterProviderSharedState(defaultResource),
7383
defaultInstrumentationScope
7484
);
7585
const meter = new Meter(meterSharedState);
76-
const observableGauge = meter.createObservableGauge('foobar');
86+
const observableGauge = meter.createObservableGauge(name);
7787
assert(observableGauge instanceof ObservableGaugeInstrument);
7888
});
7989
});
8090

8191
describe('createObservableCounter', () => {
82-
it('should create observable counter', () => {
92+
testWithNames('ObservableCounter', name => {
8393
const meterSharedState = new MeterSharedState(
8494
new MeterProviderSharedState(defaultResource),
8595
defaultInstrumentationScope
8696
);
8797
const meter = new Meter(meterSharedState);
88-
const observableCounter = meter.createObservableCounter('foobar');
98+
const observableCounter = meter.createObservableCounter(name);
8999
assert(observableCounter instanceof ObservableCounterInstrument);
90100
});
91101
});
92102

93103
describe('createObservableUpDownCounter', () => {
94-
it('should create observable up-down-counter', () => {
104+
testWithNames('ObservableUpDownCounter', name => {
95105
const meterSharedState = new MeterSharedState(
96106
new MeterProviderSharedState(defaultResource),
97107
defaultInstrumentationScope
98108
);
99109
const meter = new Meter(meterSharedState);
100-
const observableUpDownCounter =
101-
meter.createObservableUpDownCounter('foobar');
110+
const observableUpDownCounter = meter.createObservableUpDownCounter(name);
102111
assert(
103112
observableUpDownCounter instanceof ObservableUpDownCounterInstrument
104113
);
@@ -167,3 +176,22 @@ describe('Meter', () => {
167176
});
168177
});
169178
});
179+
180+
function testWithNames(type: string, tester: (name: string) => void) {
181+
for (const invalidName of invalidNames) {
182+
it(`should warn with invalid name ${invalidName} for ${type}`, () => {
183+
const warnStub = sinon.spy(diag, 'warn');
184+
tester(invalidName);
185+
assert.strictEqual(warnStub.callCount, 1);
186+
assert.ok(warnStub.calledWithMatch('Invalid metric name'));
187+
});
188+
}
189+
190+
for (const validName of validNames) {
191+
it(`should not warn with valid name ${validName} for ${type}`, () => {
192+
const warnStub = sinon.spy(diag, 'warn');
193+
tester(validName);
194+
assert.strictEqual(warnStub.callCount, 0);
195+
});
196+
}
197+
}

packages/sdk-metrics/test/util.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ export const defaultInstrumentationScope: InstrumentationScope = {
6666
schemaUrl: 'https://opentelemetry.io/schemas/1.7.0',
6767
};
6868

69+
export const invalidNames = ['', 'a'.repeat(256), '1a', '-a', '.a', '_a'];
70+
export const validNames = ['a', 'a'.repeat(255), 'a1', 'a-1', 'a.1', 'a_1'];
71+
6972
export const commonValues: number[] = [1, -1, 1.0, Infinity, -Infinity, NaN];
7073
export const commonAttributes: MetricAttributes[] = [
7174
{},

0 commit comments

Comments
 (0)