Repository navigation
fix(telemetry): write error shape, plain results and log envelope - #808
Conversation
Write every record error as {name, message} in that key order. The
plugin hooks carry only ErrorObject, which has no cause, so Python
records never have a cause. Names are unchanged.
Decode an opted-in operation result that is the default
ExtendedTypeSerDes envelope into plain JSON values. A result that
fails to decode keeps its parsed or raw value.
LambdaLogExporter now prints the Lambda JSON log envelope
{"timestamp","level":"INFO","message":"<record JSON>"}.
This comment has been minimized.
This comment has been minimized.
Decode an integral Decimal to an int only when abs(n) <= 2**53 - 1. Otherwise write a float only when it round-trips exactly and is within DynamoDB's number range (38 significant digits, magnitude 1E-130 to 9.99...E+125), else str(value). DynamoDBExporter also writes any number it cannot store, including NaN and Infinity, as a string attribute, so a decoded or plain JSON result no longer makes the item fail. LambdaLogExporter.render returns the printed envelope, so the existing single truncation pass measures the line. U+2028 and U+2029 are escaped in the line, and the size check counts them escaped. Cap every error message at 8,192 characters plus an ellipsis. Truncation first drops every cause and sets droppedCauses; Python records have no cause, so this step does not change them. Plain JSON results skip the conversion walk unless they contain NaN or an infinite number. Replace the wall-clock test assertion with a count of int conversions.
Document the Decimal rules, including negative zero, the DynamoDB string fallback, the line limit of LambdaLogExporter, the message cap, the cause truncation step, and that Python records have no cause. State that WARN filtering applies only with the JSON log format, and which envelope-shaped results are decoded or passed through.
This comment has been minimized.
This comment has been minimized.
json_byte_size encodes inside its try again, so a lone surrogate makes the size unknown and the record is kept instead of raising and being dropped by every size-limited exporter. json_byte_size is exact again: it no longer counts U+2028 and U+2029 as escapes. truncate_record takes an optional measure function, and the scheduler passes an exporter's measure when it has one. LambdaLogExporter measures the exact printed line, which escapes U+2028, U+2029 and lone surrogates as \uXXXX, so printing a lone surrogate never raises and other exporters no longer over-count.
Cap every error name, like the message, at 8,192 code points followed by an ellipsis. An int result, from an envelope or from plain JSON, whose absolute value is greater than 2**53 - 1 is written as its decimal string, like a Decimal. Plain JSON is only walked when it has a run of 16 or more digits, NaN or an infinite number. Remove the 38-digit check on the float path, which could never fail. DynamoDBExporter stores an integer with more than 38 digits but at most 38 significant digits, such as 10**38, as a number.
State that names and messages are capped at 8,192 code points, the ARN and name length assumptions behind the 64 KB statement, the 128 KB worst case, the 2**53 - 1 bound for int results, lone surrogate escaping in LambdaLogExporter, and that a DynamoDB attribute can be a number in one item and a string in another.
b5233d2 to
060c382
Compare
This comment has been minimized.
This comment has been minimized.
Resolves conflicts with the per-invocation plugin factory (#737). The error shape, result decoding and log envelope are reapplied on top of main's plugin, and the new test helpers now create the plugin through the factory.
Codex AI reviewNo actionable findings. The three prior findings are resolved. Review was static; tests were not run, and live Lambda/DynamoDB behavior was not exercised. Reviewed commit |
| # adjusted() is checked first so Decimal("1E+1000000") never builds a | ||
| # million-digit int. | ||
| if ( | ||
| value.adjusted() <= _MAX_SAFE_INTEGER_ADJUSTED |
There was a problem hiding this comment.
A Decimal zero can carry a positive exponent. Decimal("-0E+20").adjusted() is 20, so the current integer check skips it. The range check also skips it because zero is false. The float path then emits -0.0, while the README says negative zero becomes integer 0. Can this return 0 for every zero Decimal before checking adjusted()?
The existing tests cover -0 and -0.0, but neither has a large positive exponent. Adding ("-0E+20", 0) and ("0E+16", 0) would cover both signs. This one is optional.
There was a problem hiding this comment.
Confirmed on main: Decimal("-0E+20") gave -0.0 and Decimal("0E+16") gave 0.0. Fixed in #813. Every zero Decimal now returns 0 before the exponent checks. Your two cases and 0E-200 are added to the test, which also checks the type.
* fix(telemetry): write error shape, plain results and log envelope
Write every record error as {name, message} in that key order. The
plugin hooks carry only ErrorObject, which has no cause, so Python
records never have a cause. Names are unchanged.
Decode an opted-in operation result that is the default
ExtendedTypeSerDes envelope into plain JSON values. A result that
fails to decode keeps its parsed or raw value.
LambdaLogExporter now prints the Lambda JSON log envelope
{"timestamp","level":"INFO","message":"<record JSON>"}.
* fix(telemetry): bound numbers and the log line
Decode an integral Decimal to an int only when abs(n) <= 2**53 - 1.
Otherwise write a float only when it round-trips exactly and is within
DynamoDB's number range (38 significant digits, magnitude 1E-130 to
9.99...E+125), else str(value). DynamoDBExporter also writes any number
it cannot store, including NaN and Infinity, as a string attribute, so
a decoded or plain JSON result no longer makes the item fail.
LambdaLogExporter.render returns the printed envelope, so the existing
single truncation pass measures the line. U+2028 and U+2029 are
escaped in the line, and the size check counts them escaped.
Cap every error message at 8,192 characters plus an ellipsis.
Truncation first drops every cause and sets droppedCauses; Python
records have no cause, so this step does not change them.
Plain JSON results skip the conversion walk unless they contain NaN or
an infinite number. Replace the wall-clock test assertion with a count
of int conversions.
* docs(telemetry): describe number, line size and message limits
Document the Decimal rules, including negative zero, the DynamoDB
string fallback, the line limit of LambdaLogExporter, the message cap,
the cause truncation step, and that Python records have no cause.
State that WARN filtering applies only with the JSON log format, and
which envelope-shaped results are decoded or passed through.
* fix(telemetry): keep lone surrogates and measure sizes exactly
json_byte_size encodes inside its try again, so a lone surrogate makes
the size unknown and the record is kept instead of raising and being
dropped by every size-limited exporter. json_byte_size is exact again:
it no longer counts U+2028 and U+2029 as escapes.
truncate_record takes an optional measure function, and the scheduler
passes an exporter's measure when it has one. LambdaLogExporter
measures the exact printed line, which escapes U+2028, U+2029 and lone
surrogates as \uXXXX, so printing a lone surrogate never raises and
other exporters no longer over-count.
* fix(telemetry): cap error names and bound large integers
Cap every error name, like the message, at 8,192 code points followed
by an ellipsis.
An int result, from an envelope or from plain JSON, whose absolute
value is greater than 2**53 - 1 is written as its decimal string, like
a Decimal. Plain JSON is only walked when it has a run of 16 or more
digits, NaN or an infinite number. Remove the 38-digit check on the
float path, which could never fail.
DynamoDBExporter stores an integer with more than 38 digits but at most
38 significant digits, such as 10**38, as a number.
* docs(telemetry): describe name cap, int bound and size assumptions
State that names and messages are capped at 8,192 code points, the
ARN and name length assumptions behind the 64 KB statement, the 128 KB
worst case, the 2**53 - 1 bound for int results, lone surrogate
escaping in LambdaLogExporter, and that a DynamoDB attribute can be a
number in one item and a string in another.
Summary
Changes only
packages/aws-durable-execution-sdk-python-telemetry-exporter. No package version changes.nameandmessageis capped at 8,192 code points.resultwritten by the default serializer (ExtendedTypeSerDes) is decoded from its{"t","v"}envelope to plain JSON. Numbers stay exact for JavaScript readers and storable in DynamoDB.LambdaLogExporterprints the Lambda JSON log envelope, so readers that queryleveland parsemessagefind the records.max_record_size_bytescaps the printed line.The shape
The execution
error, everyoperations[].errorand everyoperationsByName.<name>.error:Keys are in the order
name,message,cause.nameis the SDK's error type and this PR does not change it.causeis present only when the SDK provides one.Python records never include
cause. The plugin hooks carry onlyErrorObject(type,message,data,stack_trace).ErrorObjecthas no cause field, and the hooks do not pass an exception, so there is no__cause__to read. The exporter does not build a cause from other operations or fromErrorData. Because no cause is ever written, the 16-level depth limit and the cycle stop have no Python code path.nameormessagelonger than 8,192 code points keeps its first 8,192 code points followed by…. A Pythonstris a sequence of code points, so a cut never splits a character. It is applied once, when the error is written.causeand setsdroppedCauses: true. The existing steps follow. Python records have no cause, so this step never changes them. It is there so the field means the same in every SDK.include_errors=Falsestill removes operation errors.Error names differ by language
Each SDK uses its own names, and they are not aligned. A step that throws
PaymentErrorcan be reported asStepErrorin JS,aws_durable_execution_sdk_python.exceptions.StepErrororPaymentErrorin Python, andStepFailedExceptionorPaymentErrorin Java. Query names per SDK, or query onmessageand thecausechain.Operation results
When the checkpointed string is the default serializer's envelope (its root has exactly the keys
tandv, andtis a string), it is decoded with that serializer and converted to plain JSON:int: unchanged whenabs(n) <= 2**53 - 1, otherwisestr(n), for example"12345678901234567".Decimal:abs(n) <= 2**53 - 1str(value), for example"9007199254740993","1E+299"or a 39-digit integernull0(documented)datetime/date→isoformat(),UUID→ string,bytes→ base64,tuple→ list,BatchResult→to_dict().null, and an integer withabs(n) > 2**53 - 1becomes its decimal string. Parsed JSON is walked only whenjson.loadssaw NaN or an infinite number, or the raw text has a run of 16 or more digits.JsonSerDesor custom result with that shape is decoded too. Only the core's known tags are decoded, and the core may coercev({"t":"b","v":"yes"}becomestrue). Unknown tags pass through unchanged. The README documents this.DynamoDBExporteralso writes any number it cannot store as a string attribute holding its JSON text. This covers numbers outside the 38-digit and magnitude range, NaN and Infinity, so a plain JSON result cannot make the item fail either. An integer with more than 38 digits but at most 38 significant digits, such as10**38, is stored as a number. The README notes that a field can beNin one item andSin another.LambdaLogExporter
Each line is
{"timestamp":"<UTC ms Z>","level":"INFO","message":"<compact record JSON>"}, with keys in that order.renderreturns this envelope, so the existing single truncation pass measures the printed line, including the second JSON escaping ofmessage. There is no second pass. The limit is in UTF-8 bytes and excludes the trailing newline.nameof normal length. Tests cover an 8,192-character message of control characters and a 70,000-character name, each at 64 KB. Without the name assumption, the worst case (name and message both 8,192 control characters) fits 128 KB (131,072 bytes), and a test covers it. The README states these assumptions.\u2028and\u2029. A lone surrogate is written as its\uXXXXescape, so printing it never raises.LambdaLogExporter.measurereturns the exact size of the printed line, escapes included, andtruncate_recordtakes it as an optionalmeasurefunction. Every other exporter is measured as the compact JSON it writes, so U+2028 and U+2029 count as 3 bytes there. A value that cannot be measured (a lone surrogate under plain UTF-8) is kept unchanged rather than raising.INFOlines are filtered only with the JSON log format and an application log level ofWARNor above.Behaviour changes
The telemetry packages are not published yet. This PR does not change any package version.
{"t","v"}envelope.LambdaLogExporterlines are the Lambda JSON log envelope, andmax_record_size_bytescaps the line, not the record.….2**53 - 1are written as strings.DynamoDBExporterwrites numbers it cannot store as strings instead of failing the item.Tests
tests/test_error_and_result_format.py:operations[].errorandoperationsByNameinclude_errors=FalseErrorObjecthas no cause fieldDecimalcases:2**53 - 1boundary,90071992547409931E+125/1E+126,1E-130/1E-131,1E+299,1E+300, a 39-digit integer1E+5000,1E±1000000-0,-0.0s,i,m,bcoercion, unknown tag)< 0.5 sassertion is replaced by an operation count: aDecimalpast the bound must convert with zeroint()calls.New
tests/test_public_path_limits.py, which goes through the public plugin path:DynamoDBExporterwith a stub client and boto3's realTypeSerializer, forDecimal("1E+299"), a 39-digit integer and9007199254740993. These are written as strings and the item is kept. Also covered: plain JSON1e300, a 39-digit int and1e-200, the DynamoDB magnitude bounds, and NaN/Infinity."a\ud800b"through the plugin intoDynamoDBExporterwith a stub client writes one item. ThroughLambdaLogExporterwith a strict UTF-8 stdout, the line is printed with\ud800escaped and round-trips. Lone surrogates are counted in the line limit.DynamoDBExporterlimit is not truncated.LambdaLogExporter.measureequals the printed line's size.2**53 - 1, negative values, nested values, and long digit runs in strings and floats left alone.DynamoDBExporterstores10**38and10**125as numbers.tests/test_shaping.py: the cause step runs first and does not mutate the input,droppedCausesis absent when there were no causes,json_byte_sizeis exact for U+2028 and returnsNonefor a lone surrogate, andtruncate_recorduses a givenmeasure.Run with the existing hatch envs:
hatch fmt --checkhatch run types:checkhatch run test:covhatch build(exporter package).github/scripts/lintcommit.py --range origin/main..HEADThe two
test:covfailures aretest_package_metadata.py::test_public_version_matches_installed_stable_releasein the telemetry-exporter and otel packages ('1.0.0' == '0.0.1'). The cause is stale installed metadata in the local test env, which was not reinstalled. They are not related to this change.Not verified
DYNAMODB_CONTEXTand the documented 1E-130 to 9.99...E+125 range.The JS and Java exporter PRs write the same error shape.