Skip to content

Commit be35e08

Browse files
committed
feat(cc-logsmap): add smart component using the new cc-api-client
Continues the Warp10 migration by wiring `cc-logsmap` to the new client: `GetHeatMapCommand` for the `heatmap` mode and `StreamRequestsCommand` for the live `points` mode. The heat map aggregates into hourly buckets, so its response is byte-identical for the rest of the current hour — cached until the next hour boundary plus a 2-minute margin to absorb ingestion lag.
1 parent 52cf148 commit be35e08

3 files changed

Lines changed: 315 additions & 0 deletions

File tree

demo-smart/index.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,11 @@
324324
width: 20em;
325325
}
326326

327+
cc-logsmap {
328+
width: 50em;
329+
height: 30em;
330+
}
331+
327332
.visually-hidden {
328333
position: absolute;
329334
width: 1px;
@@ -661,6 +666,7 @@ <h2 class="group-title">Monitoring</h2>
661666
<li><a class="definition-link" href="?definition=cc-tile-status-codes.smart">cc-tile-status-codes</a></li>
662667
<li><a class="definition-link" href="?definition=cc-tile-requests.smart">cc-tile-requests</a></li>
663668
<li><a class="definition-link" href="?definition=cc-tile-metrics.smart">cc-tile-metrics</a></li>
669+
<li><a class="definition-link" href="?definition=cc-logsmap.smart">cc-logsmap</a></li>
664670
</ul>
665671
</div>
666672

@@ -897,6 +903,15 @@ <h2 class="ctx-group-title">App</h2>
897903
<span class="ctx-label">CPU Load Bench</span>
898904
<span class="ctx-subtitle">ownerId, appId</span>
899905
</button>
906+
<button
907+
class="ctx-btn"
908+
title="ownerId, appId"
909+
data-context='{"ownerId":"orga_540caeb6-521c-4a19-a955-efe6da35d142","appId":"app_46ec6a5c-1512-401f-ad40-1864ad5050f0","consoleGrafanaLink":"https://console.clever-cloud.com","grafanaBaseLink":"https://grafana.services.clever-cloud.com/","type":"app","logsUrlPattern":"/organisations/orga_540caeb6-521c-4a19-a955-efe6da35d142/applications/:id/logs"}'
910+
>
911+
<span class="ctx-label">App Access logs</span>
912+
<span class="ctx-subtitle">ownerId, appId</span>
913+
</button>
914+
900915
</div>
901916
</div>
902917

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { GetHeatMapCommand } from '@clevercloud/client/cc-api-commands/metrics/get-heat-map-command.js';
2+
import { StreamRequestsCommand } from '@clevercloud/client/cc-api-commands/metrics/stream-requests-command.js';
3+
import { getCcApiClientWithOAuth } from '../../lib/cc-api-client.js';
4+
import { defineSmartComponent } from '../../lib/smart/define-smart-component.js';
5+
import '../cc-smart-container/cc-smart-container.js';
6+
import './cc-logsmap.js';
7+
8+
/**
9+
* @import { CcLogsmap } from './cc-logsmap.js'
10+
* @import { HeatmapPoint, MapModeType } from '../common.types.js'
11+
* @import { ApiConfig } from '../../lib/send-to-api.types.js'
12+
* @import { OnContextUpdateArgs, UpdateComponentCallback } from '../../lib/smart/smart-component.types.js'
13+
* @import { RequestsStream } from '@clevercloud/client/cc-api-commands/metrics/stream-requests-command.js'
14+
* @import { CcApiClient } from '@clevercloud/client/cc-api-client.js'
15+
*/
16+
17+
// Spread the dots of a live batch over this duration so they don't all blink at once.
18+
const POINTS_SPREAD_DURATION = 3000;
19+
// How long a live dot stays on the map before fading out (outlives the spread so dots don't vanish mid-batch).
20+
const POINTS_DELAY = POINTS_SPREAD_DURATION + 2000;
21+
22+
// The heat map aggregates requests into hourly buckets, so its response is byte-identical for the rest of the current
23+
// hour. We cache it until the next hour boundary, plus this margin to let the just-completed bucket be fully ingested
24+
// before we read it.
25+
const HEATMAP_CACHE_INGESTION_MARGIN = 2 * 60 * 1000;
26+
27+
defineSmartComponent({
28+
selector: 'cc-logsmap',
29+
params: {
30+
apiConfig: { type: Object },
31+
ownerId: { type: String },
32+
appId: { type: String, optional: true },
33+
},
34+
/**
35+
* @param {OnContextUpdateArgs<CcLogsmap>} args
36+
*/
37+
onContextUpdate({ component, context, onEvent, updateComponent, signal }) {
38+
const { apiConfig, ownerId, appId } = context;
39+
40+
const controller = new LogsmapController({ apiConfig, ownerId, appId, component, updateComponent });
41+
42+
signal.onabort = () => {
43+
controller.stop();
44+
};
45+
46+
// The user can toggle the map mode from the component itself, so we react to the change and (re)load accordingly.
47+
onEvent('cc-logsmap-mode-change', (mode) => {
48+
controller.loadMode(mode);
49+
});
50+
51+
controller.loadMode(component.mode);
52+
},
53+
});
54+
55+
/**
56+
* Drives a `<cc-logsmap>`:
57+
* * in `heatmap` mode, it fetches the heat map once with `GetHeatMapCommand`,
58+
* * in `points` mode, it opens a live SSE stream with `StreamRequestsCommand` and adds blinking dots as batches arrive.
59+
*
60+
* A generation counter guards every async callback so that stale loads (mode switched or controller stopped in the
61+
* meantime) are ignored.
62+
*/
63+
class LogsmapController {
64+
/**
65+
* @param {object} _
66+
* @param {ApiConfig} _.apiConfig
67+
* @param {string} _.ownerId
68+
* @param {string} [_.appId]
69+
* @param {CcLogsmap} _.component
70+
* @param {UpdateComponentCallback<CcLogsmap>} _.updateComponent
71+
*/
72+
constructor({ apiConfig, ownerId, appId, component, updateComponent }) {
73+
/** @type {CcApiClient} */
74+
this._client = getCcApiClientWithOAuth(apiConfig);
75+
this._ownerId = ownerId;
76+
this._appId = appId;
77+
this._component = component;
78+
this._updateComponent = updateComponent;
79+
80+
/** @type {RequestsStream|null} */
81+
this._stream = null;
82+
/** @type {AbortController|null} */
83+
this._heatmapAbortController = null;
84+
/** @type {number} */
85+
this._generation = 0;
86+
this._stopped = false;
87+
}
88+
89+
/**
90+
* Cancels the current load (live stream or heat map fetch) and invalidates its async callbacks.
91+
* @param {MapModeType} [mode]
92+
*/
93+
loadMode(mode) {
94+
this._cancel();
95+
96+
if (this._stopped) {
97+
return;
98+
}
99+
100+
if (mode === 'heatmap') {
101+
this._loadHeatmap();
102+
} else {
103+
this._openPointsStream();
104+
}
105+
}
106+
107+
stop() {
108+
this._stopped = true;
109+
this._cancel();
110+
}
111+
112+
_cancel() {
113+
// Invalidate every in-flight callback captured with the previous generation.
114+
this._generation++;
115+
116+
this._stream?.close();
117+
this._stream = null;
118+
119+
this._heatmapAbortController?.abort();
120+
this._heatmapAbortController = null;
121+
}
122+
123+
_loadHeatmap() {
124+
const generation = this._generation;
125+
this._heatmapAbortController = new AbortController();
126+
127+
this._updateComponent('error', false);
128+
this._updateComponent('loading', true);
129+
// Drop the previous result: a stale empty array would make `<cc-map>` show its "no points" message while loading.
130+
this._updateComponent('heatmapPoints', null);
131+
132+
this._client
133+
.send(new GetHeatMapCommand({ ownerId: this._ownerId, applicationId: this._appId }), {
134+
signal: this._heatmapAbortController.signal,
135+
cache: { ttl: getHeatmapCacheTtl() },
136+
})
137+
.then(
138+
/** @param {Array<HeatmapPoint>} heatmapPoints */ (heatmapPoints) => {
139+
if (generation !== this._generation) {
140+
return;
141+
}
142+
this._updateComponent('heatmapPoints', heatmapPoints);
143+
this._updateComponent('loading', false);
144+
},
145+
)
146+
.catch((error) => {
147+
if (generation !== this._generation) {
148+
return;
149+
}
150+
console.error(error);
151+
this._updateComponent('error', true);
152+
this._updateComponent('loading', false);
153+
});
154+
}
155+
156+
_openPointsStream() {
157+
const generation = this._generation;
158+
159+
this._updateComponent('error', false);
160+
this._updateComponent('loading', true);
161+
162+
this._client
163+
.stream(new StreamRequestsCommand({ ownerId: this._ownerId, applicationId: this._appId }))
164+
.then((stream) => {
165+
// Mode switched or controller stopped while the stream was being created.
166+
if (generation !== this._generation) {
167+
stream.close();
168+
return null;
169+
}
170+
171+
this._stream = stream;
172+
173+
stream.onOpen(() => {
174+
if (generation === this._generation) {
175+
this._updateComponent('loading', false);
176+
}
177+
});
178+
179+
stream.onRequests((locations) => {
180+
this._component.addPoints(
181+
locations.map((location) => ({
182+
lat: location.lat,
183+
lon: location.lon,
184+
count: location.count,
185+
tooltip: location.city,
186+
delay: POINTS_DELAY,
187+
})),
188+
{ spreadDuration: POINTS_SPREAD_DURATION },
189+
);
190+
});
191+
192+
return stream.start();
193+
})
194+
.catch((error) => {
195+
if (generation !== this._generation) {
196+
return;
197+
}
198+
console.error(error);
199+
this._updateComponent('error', true);
200+
this._updateComponent('loading', false);
201+
});
202+
}
203+
}
204+
205+
/**
206+
* Computes the heat map cache TTL (in ms) so the cached response expires shortly after the next hour boundary, once
207+
* the just-completed hourly bucket has had time to be ingested.
208+
*
209+
* @returns {number}
210+
*/
211+
function getHeatmapCacheTtl() {
212+
const now = Date.now();
213+
const nextHourBoundary = new Date(now);
214+
nextHourBoundary.setHours(nextHourBoundary.getHours() + 1, 0, 0, 0);
215+
return nextHourBoundary.getTime() - now + HEATMAP_CACHE_INGESTION_MARGIN;
216+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
kind: '🛠 Maps/<cc-logsmap>'
3+
title: '💡 Smart'
4+
---
5+
# 💡 Smart `<cc-logsmap>`
6+
7+
## ℹ️ Details
8+
9+
<table>
10+
<tr><td><strong>Component </strong> <td><a href="🛠-maps-cc-logsmap--default-story"><code>&lt;cc-logsmap&gt;</code></a>
11+
<tr><td><strong>Selector </strong> <td><code>cc-logsmap</code>
12+
<tr><td><strong>Requires auth</strong> <td>Yes
13+
</table>
14+
15+
## ⚙️ Params
16+
17+
| Name | Type | Details |
18+
|-------------|-------------|--------------------------------------------------------|
19+
| `apiConfig` | `ApiConfig` | Object with API configuration (target host, tokens...) |
20+
| `ownerId` | `String` | UUID prefixed with `user_` or `orga_` |
21+
| `appId` | `String` | UUID prefixed with `app_` (optional) |
22+
23+
```ts
24+
interface ApiConfig {
25+
API_HOST: String,
26+
API_OAUTH_TOKEN: String,
27+
API_OAUTH_TOKEN_SECRET: String,
28+
OAUTH_CONSUMER_KEY: String,
29+
OAUTH_CONSUMER_SECRET: String,
30+
}
31+
```
32+
33+
## 🌐 API endpoints
34+
35+
The endpoint used depends on the map `mode`:
36+
37+
| Method | URL | Cache | Mode |
38+
|--------|---------------------------------------------------------|-------------------------|-----------|
39+
| `GET` | `/v4/stats/organisations/{ownerId}/requests` | Until next hour + 2 min | `heatmap` |
40+
| `GET` | `/v4/stats/organisations/{ownerId}/requests-live` (SSE) | None | `points` |
41+
42+
The heat map response is byte-identical for the rest of the current hour, so it is cached until the next hour boundary plus a 2-minute margin (to absorb ingestion lag of the just-completed bucket).
43+
44+
* In `heatmap` mode, the heat map is fetched once with `GetHeatMapCommand`.
45+
* In `points` mode, a live SSE stream is opened with `StreamRequestsCommand` and blinking dots are added as request batches arrive.
46+
47+
## ⬇️️ Examples
48+
49+
### Whole organization
50+
51+
If you only specify an `ownerId` and no `appId`, the data represent the whole organization.
52+
53+
```html
54+
<cc-smart-container context='{
55+
"apiConfig": {
56+
"API_HOST": "",
57+
"API_OAUTH_TOKEN": "",
58+
"API_OAUTH_TOKEN_SECRET": "",
59+
"OAUTH_CONSUMER_KEY": "",
60+
"OAUTH_CONSUMER_SECRET": "",
61+
},
62+
"ownerId": ""
63+
}'>
64+
<cc-logsmap orga-name="ACME Corp"></cc-logsmap>
65+
</cc-smart-container>
66+
```
67+
68+
### Application only
69+
70+
```html
71+
<cc-smart-container context='{
72+
"apiConfig": {
73+
"API_HOST": "",
74+
"API_OAUTH_TOKEN": "",
75+
"API_OAUTH_TOKEN_SECRET": "",
76+
"OAUTH_CONSUMER_KEY": "",
77+
"OAUTH_CONSUMER_SECRET": "",
78+
},
79+
"ownerId": "",
80+
"appId": ""
81+
}'>
82+
<cc-logsmap app-name="My Awesome Java App (PROD)"></cc-logsmap>
83+
</cc-smart-container>
84+
```

0 commit comments

Comments
 (0)