azpaddypy 0.1.5__py3-none-any.whl → 0.1.7__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.
- {azpaddypy-0.1.5.dist-info → azpaddypy-0.1.7.dist-info}/METADATA +2 -2
- azpaddypy-0.1.7.dist-info/RECORD +9 -0
- azpaddypy-0.1.7.dist-info/top_level.txt +2 -0
- mgmt/logging.py +96 -3
- test_function/__init__.py +112 -0
- test_function/function_app.py +129 -0
- azpaddypy-0.1.5.dist-info/RECORD +0 -7
- azpaddypy-0.1.5.dist-info/top_level.txt +0 -1
- {azpaddypy-0.1.5.dist-info → azpaddypy-0.1.7.dist-info}/WHEEL +0 -0
- {azpaddypy-0.1.5.dist-info → azpaddypy-0.1.7.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,10 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: azpaddypy
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.7
|
4
4
|
Summary: Add your description here
|
5
|
-
Author-email: Patrik <patrikhartl@gmail.com>
|
6
5
|
Classifier: Operating System :: OS Independent
|
7
6
|
Requires-Python: >=3.11
|
8
7
|
Description-Content-Type: text/markdown
|
9
8
|
License-File: LICENSE
|
9
|
+
Requires-Dist: azure-monitor-opentelemetry==1.6.10
|
10
10
|
Dynamic: license-file
|
@@ -0,0 +1,9 @@
|
|
1
|
+
azpaddypy-0.1.7.dist-info/licenses/LICENSE,sha256=hQ6t0g2QaewGCQICHqTckBFbMVakGmoyTAzDpmEYV4c,1089
|
2
|
+
mgmt/__init__.py,sha256=5-0eZuJMZlCONZNJ5hEvWXfvLIM36mg7FsEVs32bY_A,183
|
3
|
+
mgmt/logging.py,sha256=9u114Ji-cwBMUf8D02i_b_Ei2dZc3TaIUH4uJv_tmSc,25451
|
4
|
+
test_function/__init__.py,sha256=0NjUl36wvUWV79GpRwBFkgkBaC6uDZsTdaSVOIHMFEU,3481
|
5
|
+
test_function/function_app.py,sha256=6nX54-iq0L1l_hZpD6E744-j79oLxdaldFyWDCpwH7c,3867
|
6
|
+
azpaddypy-0.1.7.dist-info/METADATA,sha256=-04_CkdsFyFSbUHzddoqbvL0S4cWx2BfLax8t48ueow,304
|
7
|
+
azpaddypy-0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
+
azpaddypy-0.1.7.dist-info/top_level.txt,sha256=wCjIwMZJaxbmRG_W4H2ZR9OmBJP3oKfhtvxrODAZqAo,19
|
9
|
+
azpaddypy-0.1.7.dist-info/RECORD,,
|
mgmt/logging.py
CHANGED
@@ -3,6 +3,7 @@ import os
|
|
3
3
|
import json
|
4
4
|
import functools
|
5
5
|
import time
|
6
|
+
import asyncio
|
6
7
|
from typing import Optional, Dict, Any, Union, List
|
7
8
|
from datetime import datetime
|
8
9
|
from azure.monitor.opentelemetry import configure_azure_monitor
|
@@ -317,7 +318,8 @@ class AzureLogger:
|
|
317
318
|
log_execution: bool = True,
|
318
319
|
):
|
319
320
|
"""
|
320
|
-
Decorator to automatically trace function execution with comprehensive logging
|
321
|
+
Decorator to automatically trace function execution with comprehensive logging.
|
322
|
+
Supports both synchronous and asynchronous functions.
|
321
323
|
|
322
324
|
Args:
|
323
325
|
function_name: Custom name for the span (defaults to function name)
|
@@ -328,7 +330,7 @@ class AzureLogger:
|
|
328
330
|
|
329
331
|
def decorator(func):
|
330
332
|
@functools.wraps(func)
|
331
|
-
def
|
333
|
+
async def async_wrapper(*args, **kwargs):
|
332
334
|
span_name = function_name or f"{func.__module__}.{func.__name__}"
|
333
335
|
|
334
336
|
with self.tracer.start_as_current_span(span_name) as span:
|
@@ -336,6 +338,92 @@ class AzureLogger:
|
|
336
338
|
span.set_attribute("function.name", func.__name__)
|
337
339
|
span.set_attribute("function.module", func.__module__)
|
338
340
|
span.set_attribute("service.name", self.service_name)
|
341
|
+
span.set_attribute("function.is_async", True)
|
342
|
+
|
343
|
+
if self._correlation_id:
|
344
|
+
span.set_attribute("correlation.id", self._correlation_id)
|
345
|
+
|
346
|
+
if log_args and args:
|
347
|
+
span.set_attribute("function.args_count", len(args))
|
348
|
+
if log_args and kwargs:
|
349
|
+
span.set_attribute("function.kwargs_count", len(kwargs))
|
350
|
+
|
351
|
+
start_time = time.time()
|
352
|
+
|
353
|
+
try:
|
354
|
+
self.debug(
|
355
|
+
f"Starting async function execution: {func.__name__}"
|
356
|
+
)
|
357
|
+
|
358
|
+
result = await func(*args, **kwargs)
|
359
|
+
duration_ms = (time.time() - start_time) * 1000
|
360
|
+
|
361
|
+
# Mark span as successful
|
362
|
+
span.set_attribute("function.duration_ms", duration_ms)
|
363
|
+
span.set_attribute("function.success", True)
|
364
|
+
span.set_status(Status(StatusCode.OK))
|
365
|
+
|
366
|
+
if log_result and result is not None:
|
367
|
+
span.set_attribute("function.has_result", True)
|
368
|
+
span.set_attribute(
|
369
|
+
"function.result_type", type(result).__name__
|
370
|
+
)
|
371
|
+
|
372
|
+
if log_execution:
|
373
|
+
self.log_function_execution(
|
374
|
+
func.__name__,
|
375
|
+
duration_ms,
|
376
|
+
True,
|
377
|
+
{
|
378
|
+
"args_count": len(args) if args else 0,
|
379
|
+
"kwargs_count": len(kwargs) if kwargs else 0,
|
380
|
+
"is_async": True,
|
381
|
+
},
|
382
|
+
)
|
383
|
+
|
384
|
+
self.debug(
|
385
|
+
f"Async function execution completed: {func.__name__}"
|
386
|
+
)
|
387
|
+
return result
|
388
|
+
|
389
|
+
except Exception as e:
|
390
|
+
duration_ms = (time.time() - start_time) * 1000
|
391
|
+
|
392
|
+
# Mark span as failed
|
393
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
394
|
+
span.record_exception(e)
|
395
|
+
span.set_attribute("function.duration_ms", duration_ms)
|
396
|
+
span.set_attribute("function.success", False)
|
397
|
+
span.set_attribute("error.type", type(e).__name__)
|
398
|
+
span.set_attribute("error.message", str(e))
|
399
|
+
|
400
|
+
if log_execution:
|
401
|
+
self.log_function_execution(
|
402
|
+
func.__name__,
|
403
|
+
duration_ms,
|
404
|
+
False,
|
405
|
+
{
|
406
|
+
"error_type": type(e).__name__,
|
407
|
+
"error_message": str(e),
|
408
|
+
"is_async": True,
|
409
|
+
},
|
410
|
+
)
|
411
|
+
|
412
|
+
self.error(
|
413
|
+
f"Async function execution failed: {func.__name__} - {str(e)}"
|
414
|
+
)
|
415
|
+
raise
|
416
|
+
|
417
|
+
@functools.wraps(func)
|
418
|
+
def sync_wrapper(*args, **kwargs):
|
419
|
+
span_name = function_name or f"{func.__module__}.{func.__name__}"
|
420
|
+
|
421
|
+
with self.tracer.start_as_current_span(span_name) as span:
|
422
|
+
# Add function metadata
|
423
|
+
span.set_attribute("function.name", func.__name__)
|
424
|
+
span.set_attribute("function.module", func.__module__)
|
425
|
+
span.set_attribute("service.name", self.service_name)
|
426
|
+
span.set_attribute("function.is_async", False)
|
339
427
|
|
340
428
|
if self._correlation_id:
|
341
429
|
span.set_attribute("correlation.id", self._correlation_id)
|
@@ -372,6 +460,7 @@ class AzureLogger:
|
|
372
460
|
{
|
373
461
|
"args_count": len(args) if args else 0,
|
374
462
|
"kwargs_count": len(kwargs) if kwargs else 0,
|
463
|
+
"is_async": False,
|
375
464
|
},
|
376
465
|
)
|
377
466
|
|
@@ -397,6 +486,7 @@ class AzureLogger:
|
|
397
486
|
{
|
398
487
|
"error_type": type(e).__name__,
|
399
488
|
"error_message": str(e),
|
489
|
+
"is_async": False,
|
400
490
|
},
|
401
491
|
)
|
402
492
|
|
@@ -405,7 +495,10 @@ class AzureLogger:
|
|
405
495
|
)
|
406
496
|
raise
|
407
497
|
|
408
|
-
|
498
|
+
# Return the appropriate wrapper based on whether the function is async
|
499
|
+
if asyncio.iscoroutinefunction(func):
|
500
|
+
return async_wrapper
|
501
|
+
return sync_wrapper
|
409
502
|
|
410
503
|
return decorator
|
411
504
|
|
@@ -0,0 +1,112 @@
|
|
1
|
+
import logging
|
2
|
+
import json
|
3
|
+
import time
|
4
|
+
import azure.functions as func
|
5
|
+
from azpaddypy.mgmt.logging import create_function_logger
|
6
|
+
|
7
|
+
# Initialize the logger
|
8
|
+
logger = create_function_logger(
|
9
|
+
function_app_name="test-function-app", function_name="test-function"
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
@logger.trace_function(log_args=True, log_result=True)
|
14
|
+
def process_request(req_body: dict) -> dict:
|
15
|
+
"""Process the request body and return a response"""
|
16
|
+
# Simulate some processing time
|
17
|
+
time.sleep(0.1)
|
18
|
+
|
19
|
+
# Log the request processing
|
20
|
+
logger.info(
|
21
|
+
"Processing request",
|
22
|
+
extra={
|
23
|
+
"request_id": req_body.get("request_id", "unknown"),
|
24
|
+
"action": req_body.get("action", "unknown"),
|
25
|
+
},
|
26
|
+
)
|
27
|
+
|
28
|
+
return {
|
29
|
+
"status": "success",
|
30
|
+
"message": "Request processed successfully",
|
31
|
+
"data": req_body,
|
32
|
+
}
|
33
|
+
|
34
|
+
|
35
|
+
def main(req: func.HttpRequest) -> func.HttpResponse:
|
36
|
+
"""Azure Function entry point"""
|
37
|
+
try:
|
38
|
+
# Start timing the request
|
39
|
+
start_time = time.time()
|
40
|
+
|
41
|
+
# Get request details
|
42
|
+
method = req.method
|
43
|
+
url = req.url
|
44
|
+
headers = dict(req.headers)
|
45
|
+
|
46
|
+
# Log the incoming request
|
47
|
+
logger.log_request(
|
48
|
+
method=method,
|
49
|
+
url=url,
|
50
|
+
status_code=200, # We'll update this if there's an error
|
51
|
+
duration_ms=0, # We'll update this at the end
|
52
|
+
extra={"headers": headers, "request_type": "http_trigger"},
|
53
|
+
)
|
54
|
+
|
55
|
+
# Create a span for the entire function execution
|
56
|
+
with logger.create_span("function_execution") as span:
|
57
|
+
# Add request metadata to the span
|
58
|
+
span.set_attribute("http.method", method)
|
59
|
+
span.set_attribute("http.url", url)
|
60
|
+
|
61
|
+
# Parse request body
|
62
|
+
try:
|
63
|
+
req_body = req.get_json()
|
64
|
+
except ValueError:
|
65
|
+
req_body = {}
|
66
|
+
|
67
|
+
# Log the request body
|
68
|
+
logger.info("Received request body", extra={"body": req_body})
|
69
|
+
|
70
|
+
# Process the request
|
71
|
+
result = process_request(req_body)
|
72
|
+
|
73
|
+
# Calculate request duration
|
74
|
+
duration_ms = (time.time() - start_time) * 1000
|
75
|
+
|
76
|
+
# Log successful completion
|
77
|
+
logger.log_function_execution(
|
78
|
+
function_name="main",
|
79
|
+
duration_ms=duration_ms,
|
80
|
+
success=True,
|
81
|
+
extra={"method": method, "url": url},
|
82
|
+
)
|
83
|
+
|
84
|
+
# Return the response
|
85
|
+
return func.HttpResponse(
|
86
|
+
json.dumps(result), mimetype="application/json", status_code=200
|
87
|
+
)
|
88
|
+
|
89
|
+
except Exception as e:
|
90
|
+
# Calculate request duration
|
91
|
+
duration_ms = (time.time() - start_time) * 1000
|
92
|
+
|
93
|
+
# Log the error
|
94
|
+
logger.error(
|
95
|
+
f"Error processing request: {str(e)}",
|
96
|
+
extra={"method": method, "url": url, "error_type": type(e).__name__},
|
97
|
+
)
|
98
|
+
|
99
|
+
# Log failed execution
|
100
|
+
logger.log_function_execution(
|
101
|
+
function_name="main",
|
102
|
+
duration_ms=duration_ms,
|
103
|
+
success=False,
|
104
|
+
extra={"error": str(e), "error_type": type(e).__name__},
|
105
|
+
)
|
106
|
+
|
107
|
+
# Return error response
|
108
|
+
return func.HttpResponse(
|
109
|
+
json.dumps({"status": "error", "message": str(e)}),
|
110
|
+
mimetype="application/json",
|
111
|
+
status_code=500,
|
112
|
+
)
|
@@ -0,0 +1,129 @@
|
|
1
|
+
import logging
|
2
|
+
import json
|
3
|
+
import time
|
4
|
+
import asyncio
|
5
|
+
import azure.functions as func
|
6
|
+
from azpaddypy.mgmt.logging import create_function_logger
|
7
|
+
|
8
|
+
app = func.FunctionApp()
|
9
|
+
|
10
|
+
# Initialize the logger
|
11
|
+
logger = create_function_logger(
|
12
|
+
function_app_name="test-function-app", function_name="test-function"
|
13
|
+
)
|
14
|
+
|
15
|
+
|
16
|
+
@logger.trace_function(log_args=True, log_result=True)
|
17
|
+
def process_request(req_body: dict) -> dict:
|
18
|
+
"""Process the request body and return a response"""
|
19
|
+
# Simulate some processing time
|
20
|
+
time.sleep(0.1)
|
21
|
+
|
22
|
+
# Log the request processing
|
23
|
+
logger.info(
|
24
|
+
"Processing request",
|
25
|
+
extra={
|
26
|
+
"request_id": req_body.get("request_id", "unknown"),
|
27
|
+
"action": req_body.get("action", "unknown"),
|
28
|
+
},
|
29
|
+
)
|
30
|
+
|
31
|
+
return {
|
32
|
+
"status": "success",
|
33
|
+
"message": "Request processed successfully",
|
34
|
+
"data": req_body,
|
35
|
+
}
|
36
|
+
|
37
|
+
|
38
|
+
@logger.trace_function(log_args=True, log_result=True)
|
39
|
+
async def process_request_async(req_body: dict) -> dict:
|
40
|
+
"""Process the request body asynchronously and return a response"""
|
41
|
+
# Simulate some async processing time
|
42
|
+
await asyncio.sleep(0.1)
|
43
|
+
|
44
|
+
# Log the request processing
|
45
|
+
logger.info(
|
46
|
+
"Processing async request",
|
47
|
+
extra={
|
48
|
+
"request_id": req_body.get("request_id", "unknown"),
|
49
|
+
"action": req_body.get("action", "unknown"),
|
50
|
+
"is_async": True,
|
51
|
+
},
|
52
|
+
)
|
53
|
+
|
54
|
+
return {
|
55
|
+
"status": "success",
|
56
|
+
"message": "Async request processed successfully",
|
57
|
+
"data": req_body,
|
58
|
+
}
|
59
|
+
|
60
|
+
|
61
|
+
@app.function_name(name="test-function")
|
62
|
+
@app.route(route="test-function", auth_level=func.AuthLevel.ANONYMOUS)
|
63
|
+
async def test_function(req: func.HttpRequest) -> func.HttpResponse:
|
64
|
+
"""Azure Function HTTP trigger that processes requests both synchronously and asynchronously"""
|
65
|
+
start_time = time.time()
|
66
|
+
method = req.method
|
67
|
+
url = str(req.url)
|
68
|
+
|
69
|
+
try:
|
70
|
+
# Get request body
|
71
|
+
req_body = req.get_json()
|
72
|
+
|
73
|
+
# Process request based on the action
|
74
|
+
action = req_body.get("action", "").lower()
|
75
|
+
|
76
|
+
if action == "async":
|
77
|
+
# Process request asynchronously
|
78
|
+
result = await process_request_async(req_body)
|
79
|
+
else:
|
80
|
+
# Process request synchronously
|
81
|
+
result = process_request(req_body)
|
82
|
+
|
83
|
+
# Calculate request duration
|
84
|
+
duration_ms = (time.time() - start_time) * 1000
|
85
|
+
|
86
|
+
# Log successful request
|
87
|
+
logger.log_request(
|
88
|
+
method=method,
|
89
|
+
url=url,
|
90
|
+
status_code=200,
|
91
|
+
duration_ms=duration_ms,
|
92
|
+
extra={
|
93
|
+
"request_id": req_body.get("request_id", "unknown"),
|
94
|
+
"action": action,
|
95
|
+
"is_async": action == "async",
|
96
|
+
},
|
97
|
+
)
|
98
|
+
|
99
|
+
# Return success response
|
100
|
+
return func.HttpResponse(
|
101
|
+
json.dumps(result),
|
102
|
+
mimetype="application/json",
|
103
|
+
status_code=200,
|
104
|
+
)
|
105
|
+
|
106
|
+
except Exception as e:
|
107
|
+
# Calculate request duration
|
108
|
+
duration_ms = (time.time() - start_time) * 1000
|
109
|
+
|
110
|
+
# Log the error
|
111
|
+
logger.error(
|
112
|
+
f"Error processing request: {str(e)}",
|
113
|
+
extra={"method": method, "url": url, "error_type": type(e).__name__},
|
114
|
+
)
|
115
|
+
|
116
|
+
# Log failed execution
|
117
|
+
logger.log_function_execution(
|
118
|
+
function_name="test_function",
|
119
|
+
duration_ms=duration_ms,
|
120
|
+
success=False,
|
121
|
+
extra={"error": str(e), "error_type": type(e).__name__},
|
122
|
+
)
|
123
|
+
|
124
|
+
# Return error response
|
125
|
+
return func.HttpResponse(
|
126
|
+
json.dumps({"status": "error", "message": str(e)}),
|
127
|
+
mimetype="application/json",
|
128
|
+
status_code=500,
|
129
|
+
)
|
azpaddypy-0.1.5.dist-info/RECORD
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
azpaddypy-0.1.5.dist-info/licenses/LICENSE,sha256=hQ6t0g2QaewGCQICHqTckBFbMVakGmoyTAzDpmEYV4c,1089
|
2
|
-
mgmt/__init__.py,sha256=5-0eZuJMZlCONZNJ5hEvWXfvLIM36mg7FsEVs32bY_A,183
|
3
|
-
mgmt/logging.py,sha256=jQ1jIkdSf7p44_oBJZD3rPodtwkhY5Y9rwRNafwXgcI,21061
|
4
|
-
azpaddypy-0.1.5.dist-info/METADATA,sha256=aAN8735C6AhacUNVM36ZaUKW4wwq9onAMPROaJf5Oxo,298
|
5
|
-
azpaddypy-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
6
|
-
azpaddypy-0.1.5.dist-info/top_level.txt,sha256=Xqg3gg53XEnWtdRarYe2ubJO0NDuer-pkr8RN2y7G-c,5
|
7
|
-
azpaddypy-0.1.5.dist-info/RECORD,,
|
@@ -1 +0,0 @@
|
|
1
|
-
mgmt
|
File without changes
|
File without changes
|