Skip to content

Commit 09e0a31

Browse files
committed
Refactor to create and use a DynamicLogAttributeProvider
1 parent 6cbb429 commit 09e0a31

File tree

9 files changed

+191
-40
lines changed

9 files changed

+191
-40
lines changed

packages/telemetry/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"@firebase/installations": "0.6.19",
9494
"@opentelemetry/api": "1.9.0",
9595
"@opentelemetry/api-logs": "0.203.0",
96+
"@opentelemetry/core": "2.2.0",
9697
"@opentelemetry/exporter-logs-otlp-http": "0.203.0",
9798
"@opentelemetry/otlp-exporter-base": "0.205.0",
9899
"@opentelemetry/otlp-transformer": "0.205.0",
@@ -108,7 +109,7 @@
108109
"@angular/core": "19.2.15",
109110
"@angular/platform-browser": "19.2.15",
110111
"@angular/router": "19.2.15",
111-
"@firebase/app": "0.14.4",
112+
"@firebase/app": "0.14.6",
112113
"@opentelemetry/sdk-trace-web": "2.1.0",
113114
"@rollup/plugin-json": "6.1.0",
114115
"@testing-library/dom": "10.4.1",
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { InstallationIdProvider } from './installation-id-provider';
19+
import { _FirebaseInstallationsInternal } from '@firebase/installations';
20+
import { expect } from 'chai';
21+
22+
describe('InstallationIdProvider', () => {
23+
it('should cache the FID after the first call', async () => {
24+
let callCount = 0;
25+
const mockInstallations = {
26+
getId: async () => {
27+
callCount++;
28+
return 'fid-123';
29+
}
30+
} as unknown as _FirebaseInstallationsInternal;
31+
32+
const provider = new InstallationIdProvider(mockInstallations);
33+
34+
const attr1 = await provider.getAttribute();
35+
expect(attr1).to.deep.equal(['user.id', 'fid-123']);
36+
expect(callCount).to.equal(1);
37+
38+
const attr2 = await provider.getAttribute();
39+
expect(attr2).to.deep.equal(['user.id', 'fid-123']);
40+
expect(callCount).to.equal(1); // Should still be 1
41+
});
42+
43+
it('should not cache if FID is null', async () => {
44+
let callCount = 0;
45+
let returnValue: string | null = null;
46+
const mockInstallations = {
47+
getId: async () => {
48+
callCount++;
49+
return returnValue;
50+
}
51+
} as unknown as _FirebaseInstallationsInternal;
52+
53+
const provider = new InstallationIdProvider(mockInstallations);
54+
55+
const attr1 = await provider.getAttribute();
56+
expect(attr1).to.be.null;
57+
expect(callCount).to.equal(1);
58+
59+
returnValue = 'fid-456';
60+
const attr2 = await provider.getAttribute();
61+
expect(attr2).to.deep.equal(['user.id', 'fid-456']);
62+
expect(callCount).to.equal(2);
63+
64+
// Should cache now
65+
const attr3 = await provider.getAttribute();
66+
expect(attr3).to.deep.equal(['user.id', 'fid-456']);
67+
expect(callCount).to.equal(2);
68+
});
69+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { DynamicLogAttributeProvider, LogEntryAttribute } from '../types';
19+
import { _FirebaseInstallationsInternal } from '@firebase/installations';
20+
21+
/**
22+
* An implementation of DynamicHeaderProvider that can be used to provide App Check token headers.
23+
*
24+
* @internal
25+
*/
26+
export class InstallationIdProvider implements DynamicLogAttributeProvider {
27+
private _fid: string | undefined;
28+
29+
constructor(private installationsProvider: _FirebaseInstallationsInternal) { }
30+
31+
async getAttribute(): Promise<LogEntryAttribute | null> {
32+
if (this._fid) {
33+
return ['user.id', this._fid];
34+
}
35+
36+
const fid = await this.installationsProvider.getId();
37+
if (!fid) {
38+
return null;
39+
}
40+
41+
this._fid = fid;
42+
return ['user.id', fid];
43+
}
44+
}

packages/telemetry/src/logging/logger-provider.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ import {
3030
createOtlpNetworkExportDelegate
3131
} from '@opentelemetry/otlp-exporter-base';
3232
import { FetchTransport } from './fetch-transport';
33-
import { DynamicHeaderProvider } from '../types';
33+
import { DynamicHeaderProvider, DynamicLogAttributeProvider } from '../types';
3434
import { FirebaseApp } from '@firebase/app';
35+
import { ExportResult } from '@opentelemetry/core';
36+
import { _FirebaseInstallationsInternal } from '@firebase/installations';
3537

3638
/**
3739
* Create a logger provider for the current execution environment.
@@ -41,7 +43,8 @@ import { FirebaseApp } from '@firebase/app';
4143
export function createLoggerProvider(
4244
app: FirebaseApp,
4345
endpointUrl: string,
44-
dynamicHeaderProviders: DynamicHeaderProvider[] = []
46+
dynamicHeaderProviders: DynamicHeaderProvider[] = [],
47+
dynamicLogAttributeProviders: DynamicLogAttributeProvider[] = []
4548
): LoggerProvider {
4649
const resource = resourceFromAttributes({
4750
[ATTR_SERVICE_NAME]: 'firebase_telemetry_service'
@@ -64,11 +67,38 @@ export function createLoggerProvider(
6467

6568
return new LoggerProvider({
6669
resource,
67-
processors: [new BatchLogRecordProcessor(logExporter)],
70+
processors: [new BatchLogRecordProcessor(new AsyncAttributeLogExporter(logExporter, dynamicLogAttributeProviders))],
6871
logRecordLimits: {}
6972
});
7073
}
7174

75+
/** A log exporter that appends log entries with resolved async attributes before exporting. */
76+
class AsyncAttributeLogExporter implements LogRecordExporter {
77+
private readonly _delegate: LogRecordExporter;
78+
79+
constructor(exporter: OTLPLogExporter, private dynamicLogAttributeProviders: DynamicLogAttributeProvider[]) {
80+
this._delegate = exporter;
81+
}
82+
83+
async export(logs: ReadableLogRecord[], resultCallback: (result: ExportResult) => void): Promise<void> {
84+
void Promise.all(
85+
this.dynamicLogAttributeProviders.map(async (provider) => {
86+
const attribute = await provider.getAttribute();
87+
if (attribute) {
88+
logs.forEach((log) => {
89+
log.attributes[attribute[0]] = attribute[1];
90+
});
91+
}
92+
})
93+
);
94+
void this._delegate.export(logs, resultCallback);
95+
}
96+
97+
shutdown(): Promise<void> {
98+
return this._delegate.shutdown();
99+
}
100+
}
101+
72102
/** OTLP exporter that uses custom FetchTransport. */
73103
class OTLPLogExporter
74104
extends OTLPExporterBase<ReadableLogRecord[]>

packages/telemetry/src/register.node.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ import { TELEMETRY_TYPE } from './constants';
2121
import { name, version } from '../package.json';
2222
import { TelemetryService } from './service';
2323
import { createLoggerProvider } from './logging/logger-provider';
24-
// This needs to be in the same file that calls `getProvider()` on the component
25-
// or it will get tree-shaken out.
26-
import '@firebase/installations';
2724

2825
export function registerTelemetry(): void {
2926
_registerComponent(
@@ -39,10 +36,9 @@ export function registerTelemetry(): void {
3936

4037
// getImmediate for FirebaseApp will always succeed
4138
const app = container.getProvider('app').getImmediate();
42-
const installationsProvider = container.getProvider('installations-internal').getImmediate();
4339
const loggerProvider = createLoggerProvider(app, endpointUrl);
4440

45-
return new TelemetryService(app, installationsProvider, loggerProvider);
41+
return new TelemetryService(app, loggerProvider);
4642
},
4743
ComponentType.PUBLIC
4844
).setMultipleInstances(true)

packages/telemetry/src/register.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { AppCheckProvider } from './logging/appcheck-provider';
2525
// This needs to be in the same file that calls `getProvider()` on the component
2626
// or it will get tree-shaken out.
2727
import '@firebase/installations';
28+
import { InstallationIdProvider } from './logging/installation-id-provider';
2829

2930
export function registerTelemetry(): void {
3031
_registerComponent(
@@ -43,13 +44,15 @@ export function registerTelemetry(): void {
4344
const appCheckProvider = container.getProvider('app-check-internal');
4445
const installationsProvider = container.getProvider('installations-internal').getImmediate();
4546
const dynamicHeaderProviders = [new AppCheckProvider(appCheckProvider)];
47+
const dynamicLogAttributeProviders = [new InstallationIdProvider(installationsProvider)];
4648
const loggerProvider = createLoggerProvider(
4749
app,
4850
endpointUrl,
49-
dynamicHeaderProviders
51+
dynamicHeaderProviders,
52+
dynamicLogAttributeProviders,
5053
);
5154

52-
return new TelemetryService(app, installationsProvider, loggerProvider);
55+
return new TelemetryService(app, loggerProvider);
5356
},
5457
ComponentType.PUBLIC
5558
).setMultipleInstances(true)

packages/telemetry/src/service.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,9 @@
1818
import { _FirebaseService, FirebaseApp } from '@firebase/app';
1919
import { Telemetry } from './public-types';
2020
import { LoggerProvider } from '@opentelemetry/sdk-logs';
21-
import { _FirebaseInstallationsInternal } from '@firebase/installations';
2221

2322
export class TelemetryService implements Telemetry, _FirebaseService {
24-
fid?: string;
25-
26-
constructor(public app: FirebaseApp, public installationsProvider: _FirebaseInstallationsInternal, public loggerProvider: LoggerProvider) {
27-
void this._getFid();
28-
}
29-
30-
private async _getFid(): Promise<void> {
31-
try {
32-
const fid = await this.installationsProvider.getId();
33-
this.fid = fid;
34-
} catch (err) {
35-
console.error('Failed to get FID for telemetry:', err);
36-
}
37-
}
23+
constructor(public app: FirebaseApp, public loggerProvider: LoggerProvider) {}
3824

3925
_delete(): Promise<void> {
4026
return Promise.resolve();

packages/telemetry/src/types.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,38 @@
1515
* limitations under the License.
1616
*/
1717

18+
type KeyValuePair = [key: string, value: string];
19+
20+
/**
21+
* A type for Cloud Logging log entry attributes
22+
*
23+
* @internal
24+
*/
25+
export type LogEntryAttribute = KeyValuePair;
26+
27+
/**
28+
* An interface for classes that provide dynamic log entry attributes.
29+
*
30+
* Classes that implement this interface can be used to supply custom headers for logging.
31+
*
32+
* @internal
33+
*/
34+
export interface DynamicLogAttributeProvider {
35+
/**
36+
* Returns a record of attributes to be added to a log entry.
37+
*
38+
* @returns A {@link Promise} that resolves to a {@link LogEntryAttribute} key-value pair,
39+
* or null if no attribute is to be added.
40+
*/
41+
getAttribute(): Promise<LogEntryAttribute | null>;
42+
}
43+
1844
/**
1945
* A type for HTTP Headers
2046
*
2147
* @internal
2248
*/
23-
export type HttpHeader = [key: string, value: string];
49+
export type HttpHeader = KeyValuePair;
2450

2551
/**
2652
* An interface for classes that provide dynamic headers.
@@ -33,8 +59,8 @@ export interface DynamicHeaderProvider {
3359
/**
3460
* Returns a record of headers to be added to a request.
3561
*
36-
* @returns A {@link Promise} that resolves to a {@link Record<string, string>} of header
37-
* key-value pairs, or null if no headers are to be added.
62+
* @returns A {@link Promise} that resolves to a {@link HttpHeader} key-value pair,
63+
* or null if no header is to be added.
3864
*/
3965
getHeader(): Promise<HttpHeader | null>;
4066
}

yarn.lock

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,17 +1360,6 @@
13601360
resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
13611361
integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
13621362

1363-
"@firebase/app@0.14.4":
1364-
version "0.14.4"
1365-
resolved "https://registry.npmjs.org/@firebase/app/-/app-0.14.4.tgz#1d2ce74c09752dec9664e2f981b20335c4efbec1"
1366-
integrity sha512-pUxEGmR+uu21OG/icAovjlu1fcYJzyVhhT0rsCrn+zi+nHtrS43Bp9KPn9KGa4NMspCUE++nkyiqziuIvJdwzw==
1367-
dependencies:
1368-
"@firebase/component" "0.7.0"
1369-
"@firebase/logger" "0.5.0"
1370-
"@firebase/util" "1.13.0"
1371-
idb "7.1.1"
1372-
tslib "^2.1.0"
1373-
13741363
"@gar/promisify@^1.0.1":
13751364
version "1.1.3"
13761365
resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
@@ -2788,6 +2777,13 @@
27882777
dependencies:
27892778
"@opentelemetry/semantic-conventions" "^1.29.0"
27902779

2780+
"@opentelemetry/core@2.2.0":
2781+
version "2.2.0"
2782+
resolved "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz#2f857d7790ff160a97db3820889b5f4cade6eaee"
2783+
integrity sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==
2784+
dependencies:
2785+
"@opentelemetry/semantic-conventions" "^1.29.0"
2786+
27912787
"@opentelemetry/exporter-logs-otlp-http@0.203.0":
27922788
version "0.203.0"
27932789
resolved "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.203.0.tgz#cdecb5c5b39561aa8520c8bb78347c6e11c91a81"

0 commit comments

Comments
 (0)