langfun 0.1.2.dev202509290805__py3-none-any.whl → 0.1.2.dev202509300805__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of langfun might be problematic. Click here for more details.

@@ -0,0 +1,545 @@
1
+ # Copyright 2025 The Langfun Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Environment event logger."""
15
+
16
+ from langfun.env.event_handlers import base
17
+ import pyglove as pg
18
+
19
+
20
+ _METRIC_NAMESPACE = '/langfun/env'
21
+
22
+
23
+ class MetricWriter(pg.Object, base.EventHandler):
24
+ """Event handler for streamz metrics."""
25
+
26
+ def _get_counter(
27
+ self,
28
+ name: str,
29
+ description: str,
30
+ parameters: dict[str, type[str]] | None = None
31
+ ) -> pg.monitoring.Counter:
32
+ return self._metric_collection.get_counter(
33
+ name=name,
34
+ description=description,
35
+ parameters=parameters
36
+ )
37
+
38
+ def _get_scalar(
39
+ self,
40
+ name: str,
41
+ description: str,
42
+ parameters: dict[str, type[str]] | None = None
43
+ ) -> pg.monitoring.Metric:
44
+ return self._metric_collection.get_scalar(
45
+ name=name,
46
+ description=description,
47
+ parameters=parameters
48
+ )
49
+
50
+ def _initialize_metrics(self) -> None:
51
+ """Initializes metrics."""
52
+
53
+ self._metric_collection = pg.monitoring.metric_collection(_METRIC_NAMESPACE)
54
+
55
+ #
56
+ # Environment metrics.
57
+ #
58
+
59
+ self._environment_housekeep_duration_ms = self._get_scalar(
60
+ 'environment_housekeep_duration_ms',
61
+ description='Environment housekeeping duration in milliseconds',
62
+ parameters={
63
+ 'environment_id': str,
64
+ 'has_error': bool,
65
+ }
66
+ )
67
+
68
+ #
69
+ # Sandbox metrics.
70
+ #
71
+
72
+ # Sandbox counters.
73
+ self._sandbox_start = self._get_counter(
74
+ 'sandbox_start',
75
+ description='Sandbox start counter',
76
+ parameters={
77
+ 'environment_id': str,
78
+ 'has_error': bool,
79
+ }
80
+ )
81
+ self._sandbox_shutdown = self._get_counter(
82
+ 'sandbox_shutdown',
83
+ description='Sandbox shutdown counter',
84
+ parameters={
85
+ 'environment_id': str,
86
+ 'has_error': bool
87
+ }
88
+ )
89
+ self._sandbox_housekeep = self._get_counter(
90
+ 'sandbox_housekeep',
91
+ description='Sandbox housekeeping counter',
92
+ parameters={
93
+ 'environment_id': str,
94
+ 'has_error': bool,
95
+ }
96
+ )
97
+ self._sandbox_activity = self._get_counter(
98
+ 'sandbox_activity',
99
+ description='Sandbox activity counter',
100
+ parameters={
101
+ 'environment_id': str,
102
+ 'activity': str,
103
+ 'has_error': bool,
104
+ }
105
+ )
106
+
107
+ # Sandbox scalars.
108
+ self._sandbox_lifetime_ms = self._get_scalar(
109
+ 'sandbox_lifetime_ms',
110
+ description='Sandbox life time in milliseconds',
111
+ parameters={
112
+ 'environment_id': str,
113
+ 'has_error': bool,
114
+ }
115
+ )
116
+ self._sandbox_start_duration_ms = self._get_scalar(
117
+ 'sandbox_start_duration_ms',
118
+ description='Sandbox start duration in milliseconds',
119
+ parameters={
120
+ 'environment_id': str,
121
+ 'has_error': bool,
122
+ }
123
+ )
124
+ self._sandbox_shutdown_duration_ms = self._get_scalar(
125
+ 'sandbox_shutdown_duration_ms',
126
+ description='Sandbox shutdown duration in milliseconds',
127
+ parameters={
128
+ 'environment_id': str,
129
+ 'has_error': bool,
130
+ }
131
+ )
132
+ self._sandbox_housekeep_duration_ms = self._get_scalar(
133
+ 'sandbox_housekeep_duration_ms',
134
+ description='Sandbox housekeeping duration in milliseconds',
135
+ parameters={
136
+ 'environment_id': str,
137
+ 'has_error': bool,
138
+ }
139
+ )
140
+ self._sandbox_status_duration_ms = self._get_scalar(
141
+ 'sandbox_status_duration_ms',
142
+ description='Sandbox duration of specific status in milliseconds',
143
+ parameters={
144
+ 'environment_id': str,
145
+ 'status': str,
146
+ }
147
+ )
148
+ self._sandbox_activity_duration_ms = self._get_scalar(
149
+ 'sandbox_activity_duration_ms',
150
+ description='Sandbox activity duration in milliseconds',
151
+ parameters={
152
+ 'environment_id': str,
153
+ 'activity': str,
154
+ 'has_error': bool,
155
+ }
156
+ )
157
+
158
+ #
159
+ # Feature metrics.
160
+ #
161
+
162
+ # Feature counters.
163
+ self._feature_setup = self._get_counter(
164
+ 'feature_setup',
165
+ description='Feature setup counter',
166
+ parameters={
167
+ 'environment_id': str,
168
+ 'feature_name': str,
169
+ 'has_error': bool,
170
+ }
171
+ )
172
+ self._feature_teardown = self._get_counter(
173
+ 'feature_teardown',
174
+ description='Feature teardown counter',
175
+ parameters={
176
+ 'environment_id': str,
177
+ 'feature_name': str,
178
+ 'has_error': bool,
179
+ }
180
+ )
181
+ self._feature_setup_session = self._get_counter(
182
+ 'feature_setup_session',
183
+ description='Feature setup session counter',
184
+ parameters={
185
+ 'environment_id': str,
186
+ 'feature_name': str,
187
+ 'has_error': bool,
188
+ }
189
+ )
190
+ self._feature_teardown_session = self._get_counter(
191
+ 'feature_teardown_session',
192
+ description='Feature teardown session counter',
193
+ parameters={
194
+ 'environment_id': str,
195
+ 'feature_name': str,
196
+ 'has_error': bool,
197
+ }
198
+ )
199
+ self._feature_housekeep = self._get_counter(
200
+ 'feature_housekeep',
201
+ description='Feature housekeeping counter',
202
+ parameters={
203
+ 'environment_id': str,
204
+ 'feature_name': str,
205
+ 'has_error': bool,
206
+ }
207
+ )
208
+
209
+ # Feature scalars.
210
+ self._feature_setup_duration_ms = self._get_scalar(
211
+ 'feature_setup_duration_ms',
212
+ description='Feature setup duration in milliseconds',
213
+ parameters={
214
+ 'environment_id': str,
215
+ 'feature_name': str,
216
+ 'has_error': bool,
217
+ }
218
+ )
219
+ self._feature_teardown_duration_ms = self._get_scalar(
220
+ 'feature_teardown_duration_ms',
221
+ description='Feature teardown duration in milliseconds',
222
+ parameters={
223
+ 'environment_id': str,
224
+ 'feature_name': str,
225
+ 'has_error': bool,
226
+ }
227
+ )
228
+ self._feature_setup_session_duration_ms = self._get_scalar(
229
+ 'feature_setup_session_duration_ms',
230
+ description='Feature setup session duration in milliseconds',
231
+ parameters={
232
+ 'environment_id': str,
233
+ 'feature_name': str,
234
+ 'has_error': bool,
235
+ }
236
+ )
237
+ self._feature_teardown_session_duration_ms = self._get_scalar(
238
+ 'feature_teardown_session_duration_ms',
239
+ description='Feature teardown session duration in milliseconds',
240
+ parameters={
241
+ 'environment_id': str,
242
+ 'feature_name': str,
243
+ 'has_error': bool,
244
+ }
245
+ )
246
+ self._feature_housekeep_duration_ms = self._get_scalar(
247
+ 'feature_housekeep_duration_ms',
248
+ description='Feature housekeeping duration in milliseconds',
249
+ parameters={
250
+ 'environment_id': str,
251
+ 'feature_name': str,
252
+ 'has_error': bool,
253
+ }
254
+ )
255
+
256
+ #
257
+ # Session metrics.
258
+ #
259
+
260
+ self._session_start_duration_ms = self._get_scalar(
261
+ 'session_start_duration_ms',
262
+ description='Session start duration in milliseconds',
263
+ parameters={
264
+ 'environment_id': str,
265
+ 'has_error': bool,
266
+ }
267
+ )
268
+ self._session_end_duration_ms = self._get_scalar(
269
+ 'session_end_duration_ms',
270
+ description='Session end duration in milliseconds',
271
+ parameters={
272
+ 'environment_id': str,
273
+ 'has_error': bool,
274
+ }
275
+ )
276
+ self._session_lifetime_ms = self._get_scalar(
277
+ 'session_lifetime_ms',
278
+ description='Session lifetime in milliseconds',
279
+ parameters={
280
+ 'environment_id': str,
281
+ 'has_error': bool,
282
+ }
283
+ )
284
+
285
+ def on_environment_starting(
286
+ self,
287
+ environment: base.Environment
288
+ ) -> None:
289
+ """Called when the environment is starting."""
290
+ self._initialize_metrics()
291
+
292
+ def on_environment_housekeep(
293
+ self,
294
+ environment: base.Environment,
295
+ counter: int,
296
+ duration: float,
297
+ error: BaseException | None
298
+ ) -> None:
299
+ """Called when the environment is housekeeping."""
300
+ self._environment_housekeep_duration_ms.record(
301
+ int(duration * 1000),
302
+ environment_id=str(environment.id),
303
+ has_error=error is not None
304
+ )
305
+
306
+ def on_sandbox_start(
307
+ self,
308
+ environment: base.Environment,
309
+ sandbox: base.Sandbox,
310
+ duration: float,
311
+ error: BaseException | None
312
+ ) -> None:
313
+ self._sandbox_start.increment(
314
+ environment_id=str(environment.id),
315
+ has_error=error is not None
316
+ )
317
+ self._sandbox_start_duration_ms.record(
318
+ int(duration * 1000),
319
+ environment_id=str(environment.id),
320
+ has_error=error is not None
321
+ )
322
+
323
+ def on_sandbox_status_change(
324
+ self,
325
+ environment: base.Environment,
326
+ sandbox: base.Sandbox,
327
+ old_status: base.Sandbox.Status,
328
+ new_status: base.Sandbox.Status,
329
+ span: float
330
+ ) -> None:
331
+ self._sandbox_status_duration_ms.record(
332
+ int(span * 1000),
333
+ environment_id=str(environment.id),
334
+ status=old_status.value
335
+ )
336
+
337
+ def on_sandbox_shutdown(
338
+ self,
339
+ environment: base.Environment,
340
+ sandbox: base.Sandbox,
341
+ duration: float,
342
+ lifetime: float,
343
+ error: BaseException | None
344
+ ) -> None:
345
+ self._sandbox_shutdown.increment(
346
+ environment_id=str(environment.id),
347
+ has_error=error is not None
348
+ )
349
+ self._sandbox_shutdown_duration_ms.record(
350
+ int(duration * 1000),
351
+ environment_id=str(environment.id),
352
+ has_error=error is not None
353
+ )
354
+ self._sandbox_lifetime_ms.record(
355
+ int(lifetime * 1000),
356
+ environment_id=str(environment.id),
357
+ has_error=error is not None
358
+ )
359
+
360
+ def on_sandbox_housekeep(
361
+ self,
362
+ environment: base.Environment,
363
+ sandbox: base.Sandbox,
364
+ counter: int,
365
+ duration: float,
366
+ error: BaseException | None
367
+ ) -> None:
368
+ """Called when a sandbox feature is housekeeping."""
369
+ self._sandbox_housekeep.increment(
370
+ environment_id=str(environment.id),
371
+ has_error=error is not None
372
+ )
373
+ self._sandbox_housekeep_duration_ms.record(
374
+ int(duration * 1000),
375
+ environment_id=str(environment.id),
376
+ has_error=error is not None
377
+ )
378
+
379
+ def on_feature_setup(
380
+ self,
381
+ environment: base.Environment,
382
+ sandbox: base.Sandbox,
383
+ feature: base.Feature,
384
+ duration: float,
385
+ error: BaseException | None
386
+ ) -> None:
387
+ """Called when a sandbox feature is setup."""
388
+ self._feature_setup.increment(
389
+ environment_id=str(environment.id),
390
+ feature_name=feature.name,
391
+ has_error=error is not None
392
+ )
393
+ self._feature_setup_duration_ms.record(
394
+ int(duration * 1000),
395
+ environment_id=str(environment.id),
396
+ feature_name=feature.name,
397
+ has_error=error is not None
398
+ )
399
+
400
+ def on_feature_teardown(
401
+ self,
402
+ environment: base.Environment,
403
+ sandbox: base.Sandbox,
404
+ feature: base.Feature,
405
+ duration: float,
406
+ error: BaseException | None
407
+ ) -> None:
408
+ """Called when a sandbox feature is teardown."""
409
+ self._feature_teardown.increment(
410
+ environment_id=str(environment.id),
411
+ feature_name=feature.name,
412
+ has_error=error is not None
413
+ )
414
+ self._feature_teardown_duration_ms.record(
415
+ int(duration * 1000),
416
+ environment_id=str(environment.id),
417
+ feature_name=feature.name,
418
+ has_error=error is not None
419
+ )
420
+
421
+ def on_feature_setup_session(
422
+ self,
423
+ environment: base.Environment,
424
+ sandbox: base.Sandbox,
425
+ feature: base.Feature,
426
+ session_id: str | None,
427
+ duration: float,
428
+ error: BaseException | None
429
+ ) -> None:
430
+ """Called when a sandbox feature is setup."""
431
+ self._feature_setup_session.increment(
432
+ environment_id=str(environment.id),
433
+ feature_name=feature.name,
434
+ has_error=error is not None
435
+ )
436
+ self._feature_setup_session_duration_ms.record(
437
+ int(duration * 1000),
438
+ environment_id=str(environment.id),
439
+ feature_name=feature.name,
440
+ has_error=error is not None
441
+ )
442
+
443
+ def on_feature_teardown_session(
444
+ self,
445
+ environment: base.Environment,
446
+ sandbox: base.Sandbox,
447
+ feature: base.Feature,
448
+ session_id: str,
449
+ duration: float,
450
+ error: BaseException | None
451
+ ) -> None:
452
+ """Called when a sandbox feature is teardown."""
453
+ self._feature_teardown_session.increment(
454
+ environment_id=str(environment.id),
455
+ feature_name=feature.name,
456
+ has_error=error is not None
457
+ )
458
+ self._feature_teardown_session_duration_ms.record(
459
+ int(duration * 1000),
460
+ environment_id=str(environment.id),
461
+ feature_name=feature.name,
462
+ has_error=error is not None
463
+ )
464
+
465
+ def on_feature_housekeep(
466
+ self,
467
+ environment: base.Environment,
468
+ sandbox: base.Sandbox,
469
+ feature: base.Feature,
470
+ counter: int,
471
+ duration: float,
472
+ error: BaseException | None
473
+ ) -> None:
474
+ """Called when a sandbox feature is housekeeping."""
475
+ self._feature_housekeep.increment(
476
+ environment_id=str(environment.id),
477
+ feature_name=feature.name,
478
+ has_error=error is not None
479
+ )
480
+ self._feature_housekeep_duration_ms.record(
481
+ int(duration * 1000),
482
+ environment_id=str(environment.id),
483
+ feature_name=feature.name,
484
+ has_error=error is not None
485
+ )
486
+
487
+ def on_session_start(
488
+ self,
489
+ environment: base.Environment,
490
+ sandbox: base.Sandbox,
491
+ session_id: str,
492
+ duration: float,
493
+ error: BaseException | None
494
+ ) -> None:
495
+ """Called when a sandbox session starts."""
496
+ self._session_start_duration_ms.record(
497
+ int(duration * 1000),
498
+ environment_id=str(environment.id),
499
+ has_error=error is not None
500
+ )
501
+
502
+ def on_session_end(
503
+ self,
504
+ environment: base.Environment,
505
+ sandbox: base.Sandbox,
506
+ session_id: str,
507
+ duration: float,
508
+ lifetime: float,
509
+ error: BaseException | None
510
+ ) -> None:
511
+ """Called when a sandbox session ends."""
512
+ self._session_end_duration_ms.record(
513
+ int(duration * 1000),
514
+ environment_id=str(environment.id),
515
+ has_error=error is not None
516
+ )
517
+ self._session_lifetime_ms.record(
518
+ int(lifetime * 1000),
519
+ environment_id=str(environment.id),
520
+ has_error=error is not None
521
+ )
522
+
523
+ def on_sandbox_activity(
524
+ self,
525
+ name: str,
526
+ environment: base.Environment,
527
+ sandbox: base.Sandbox,
528
+ feature: base.Feature | None,
529
+ session_id: str | None,
530
+ duration: float,
531
+ error: BaseException | None,
532
+ **kwargs
533
+ ) -> None:
534
+ """Called when a sandbox activity is performed."""
535
+ self._sandbox_activity.increment(
536
+ environment_id=str(environment.id),
537
+ activity=name,
538
+ has_error=error is not None
539
+ )
540
+ self._sandbox_activity_duration_ms.record(
541
+ int(duration * 1000),
542
+ environment_id=str(environment.id),
543
+ activity=name,
544
+ has_error=error is not None
545
+ )
@@ -0,0 +1,160 @@
1
+ # Copyright 2025 The Langfun Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import unittest
16
+
17
+ from langfun.env import interface
18
+ from langfun.env import test_utils
19
+ from langfun.env.event_handlers import metric_writer as metric_writer_lib
20
+
21
+
22
+ class MetricWriterTest(unittest.TestCase):
23
+
24
+ def test_write_metric(self):
25
+ writer = metric_writer_lib.MetricWriter()
26
+ env = test_utils.TestingEnvironment(
27
+ features={
28
+ 'test_feature1': test_utils.TestingFeature(housekeep_interval=0),
29
+ 'test_feature2': test_utils.TestingFeature(housekeep_interval=None),
30
+ },
31
+ pool_size=2,
32
+ outage_grace_period=0,
33
+ outage_retry_interval=0,
34
+ housekeep_interval=0.0,
35
+ sandbox_keepalive_interval=1.0,
36
+ event_handlers=[writer],
37
+ )
38
+ with env:
39
+ with env.sandbox('session1') as sb:
40
+ self.assertEqual(sb.test_feature1.num_shell_calls(), 4)
41
+
42
+ with self.assertRaises(interface.SandboxStateError):
43
+ with env.sandbox('session2') as sb:
44
+ sb.shell('echo "bar"', raise_error=RuntimeError)
45
+
46
+ self.assertEqual(
47
+ writer._sandbox_start.value(
48
+ environment_id='testing-env', has_error=False
49
+ ),
50
+ 2
51
+ )
52
+ self.assertGreater(
53
+ writer._sandbox_housekeep.value(
54
+ environment_id='testing-env',
55
+ has_error=False
56
+ ),
57
+ 0,
58
+ )
59
+ self.assertEqual(
60
+ writer._sandbox_shutdown.value(
61
+ environment_id='testing-env', has_error=False
62
+ ),
63
+ 2
64
+ )
65
+ self.assertEqual(
66
+ writer._feature_setup.value(
67
+ environment_id='testing-env',
68
+ feature_name='test_feature1',
69
+ has_error=False
70
+ ),
71
+ 2,
72
+ )
73
+ self.assertEqual(
74
+ writer._feature_setup.value(
75
+ environment_id='testing-env',
76
+ feature_name='test_feature2',
77
+ has_error=False
78
+ ),
79
+ 2,
80
+ )
81
+ self.assertEqual(
82
+ writer._feature_setup_session.value(
83
+ environment_id='testing-env',
84
+ feature_name='test_feature1',
85
+ has_error=False
86
+ ),
87
+ 3,
88
+ )
89
+ self.assertEqual(
90
+ writer._feature_setup_session.value(
91
+ environment_id='testing-env',
92
+ feature_name='test_feature2',
93
+ has_error=False
94
+ ),
95
+ 3,
96
+ )
97
+ self.assertEqual(
98
+ writer._feature_teardown_session.value(
99
+ environment_id='testing-env',
100
+ feature_name='test_feature1',
101
+ has_error=False
102
+ ),
103
+ 2,
104
+ )
105
+ self.assertEqual(
106
+ writer._feature_teardown_session.value(
107
+ environment_id='testing-env',
108
+ feature_name='test_feature2',
109
+ has_error=False
110
+ ),
111
+ 2,
112
+ )
113
+ self.assertEqual(
114
+ writer._feature_teardown.value(
115
+ environment_id='testing-env',
116
+ feature_name='test_feature1',
117
+ has_error=False
118
+ ),
119
+ 2,
120
+ )
121
+ self.assertEqual(
122
+ writer._feature_teardown.value(
123
+ environment_id='testing-env',
124
+ feature_name='test_feature2',
125
+ has_error=False
126
+ ),
127
+ 2,
128
+ )
129
+ self.assertGreater(
130
+ writer._feature_housekeep.value(
131
+ environment_id='testing-env',
132
+ feature_name='test_feature1',
133
+ has_error=False
134
+ ),
135
+ 0,
136
+ )
137
+ self.assertEqual(
138
+ writer._feature_housekeep.value(
139
+ environment_id='testing-env',
140
+ feature_name='test_feature2',
141
+ has_error=False
142
+ ),
143
+ 0,
144
+ )
145
+ self.assertEqual(
146
+ writer._sandbox_activity.value(
147
+ environment_id='testing-env', activity='shell', has_error=False
148
+ ),
149
+ 18
150
+ )
151
+ self.assertEqual(
152
+ writer._sandbox_activity.value(
153
+ environment_id='testing-env', activity='shell', has_error=True
154
+ ),
155
+ 1
156
+ )
157
+
158
+
159
+ if __name__ == '__main__':
160
+ unittest.main()