Extension
Extension runs the 10x Engine as its own Lambda extension inside each function's execution environment, paired over loopback with the OTel collector extension the function already runs. The collector hands every log record to the engine and receives it back regulated, routed, and hashed. No code changes, no new infrastructure, and nothing leaves the sandbox.
This launcher employs the Runtime flavor (native binary).
Use-case:
Per-pattern regulation on serverless estates. A 100% Lambda shop has no node, no DaemonSet, and no forwarder host to install onto; the execution environment itself is the edge. The extension applies the same receive pipeline the Receiver runs on Kubernetes: tag noisy patterns for cheaper storage tiers, offload slices to your own S3, and mute what nobody reads, before the first byte leaves the function.
Benefits
A Layer, Not a Rebuild
The engine ships as a single Lambda layer: the native bootstrap under extensions/, with modules, config, and the symbol library under /opt/tenx. Attaching it to a function is a layer ARN plus environment variables; removing it is removing them. The layer unpacks to ~180 MB of Lambda's 250 MB shared layer budget.
Per-Pattern Control
Dispositions are keyed by pattern identity (message_pattern), not by function. A mute file decides each pattern's rate, with an explicit expiry and reason per entry, and a severity retention floor keeps ERROR/FATAL traffic flowing even under a full mute.
Policy Without Redeploys
TENX_RECEIVE_MUTE_S3_URI points the extension at a mute file in your own bucket. It fetches at INIT and refreshes on invoke (default 5 minutes), so one aws s3 cp converges every function. A failed fetch keeps the last good copy; dispositions degrade to the previous state, never to none.
Nothing Leaves the Sandbox
The collector pairing is loopback-only, and TENX_AIRGAPPED=true silences all engine egress to log10x. The only network path the extension adds is to your own S3 bucket, and only when you configure one.
Architecture Flow
graph LR
A["Function<br/>Code"] -->|"Logs"| B["OTel Collector<br/>Extension"]
B -->|"1. OTLP<br/>127.0.0.1:4317"| C["10x Engine<br/>Extension"]
C -->|"2. Regulated events<br/>127.0.0.1:24225"| B
B -->|"3. routeState<br/>routing"| D["Destination<br/>(SIEM)"]
B -->|"offload slice"| E["Your S3<br/>Bucket"]
classDef aws fill:#ff9900,stroke:#cc7a00,color:#ffffff,stroke-width:2px,rx:8,ry:8
classDef tenx fill:#059669,stroke:#047857,color:#ffffff,stroke-width:2px,rx:8,ry:8
classDef forwarder fill:#3b82f6,stroke:#1d4ed8,color:#ffffff,stroke-width:2px,rx:8,ry:8
classDef destination fill:#6b7280,stroke:#4b5563,color:#ffffff,stroke-width:2px,rx:8,ry:8
class A aws
class B forwarder
class C tenx
class D destination
class E aws
1. The function logs as it always has. The collector extension picks events up through its existing receivers.
2. The collector hands each record to the engine over loopback OTLP/gRPC (127.0.0.1:4317) and receives it back on 127.0.0.1:24225, now carrying tenx_hash (stable pattern identity) and routeState (the pattern's disposition) as log attributes.
3. A routing connector fans out on routeState: the SIEM slice ships through the existing exporter unchanged, the offload slice lands in your S3 bucket in the layout the Retriever indexes, and drops end at the sandbox.
Deployment
The log10x MCP emits this entire plan from a discovery snapshot (Advise returns it directly on a serverless estate, including a CDK construct). The steps below are the manual path.
Step 1: Prerequisites
| Requirement | Description |
|---|---|
| Log10x License | A full (non-demo) license JWT (get one), passed as TENX_LICENSE_KEY. Demo licenses cannot run airgapped, and TENX_AIRGAPPED=true is required here |
| AWS CLI | AWS CLI v2 configured with Lambda and S3 permissions |
| OTel Collector Extension | The target functions already run a collector extension with routing and decouple components (the community Lambda collector layer has both; a minimal custom build may not) |
| S3 Bucket | Optional. One bucket serves both the offload slice and live mute-file delivery |
Step 2: Pick the Layer ARN
The layer is published in every region below, for both architectures, and is readable by any AWS account. Nothing to build:
| Region | x86_64 | arm64 |
|---|---|---|
us-east-1 |
arn:aws:lambda:us-east-1:351939435334:layer:tenx-receive:6 |
arn:aws:lambda:us-east-1:351939435334:layer:tenx-receive-arm64:2 |
us-east-2 |
arn:aws:lambda:us-east-2:351939435334:layer:tenx-receive:2 |
arn:aws:lambda:us-east-2:351939435334:layer:tenx-receive-arm64:2 |
us-west-2 |
arn:aws:lambda:us-west-2:351939435334:layer:tenx-receive:2 |
arn:aws:lambda:us-west-2:351939435334:layer:tenx-receive-arm64:2 |
eu-west-1 |
arn:aws:lambda:eu-west-1:351939435334:layer:tenx-receive:2 |
arn:aws:lambda:eu-west-1:351939435334:layer:tenx-receive-arm64:2 |
eu-central-1 |
arn:aws:lambda:eu-central-1:351939435334:layer:tenx-receive:2 |
arn:aws:lambda:eu-central-1:351939435334:layer:tenx-receive-arm64:2 |
ap-southeast-1 |
arn:aws:lambda:ap-southeast-1:351939435334:layer:tenx-receive:2 |
arn:aws:lambda:ap-southeast-1:351939435334:layer:tenx-receive-arm64:2 |
ap-northeast-1 |
arn:aws:lambda:ap-northeast-1:351939435334:layer:tenx-receive:2 |
arn:aws:lambda:ap-northeast-1:351939435334:layer:tenx-receive-arm64:2 |
ap-south-1 |
arn:aws:lambda:ap-south-1:351939435334:layer:tenx-receive:2 |
arn:aws:lambda:ap-south-1:351939435334:layer:tenx-receive-arm64:2 |
Match the architecture to the function's own: arm64 for Graviton,
x86_64 otherwise. A mismatch fails at INIT rather than silently.
The layer carries the engine, modules, config, and symbol library. It carries no license, so the license travels as an environment variable in the next step.
Step 3: Attach the Layer and Environment
Write the environment as a JSON file. TENX_RECEIVE_APPS contains commas, which the AWS CLI's --environment Variables={...} shorthand silently mis-parses; file:// is the reliable form.
{
"Variables": {
"TENX_MODULES": "/opt/tenx/modules",
"TENX_CONFIG": "/opt/tenx/config",
"TENX_SYMBOLS_PATH": "/opt/tenx/symbols",
"TENX_LICENSE_KEY": "<your license JWT>",
"TENX_AIRGAPPED": "true",
"TENX_LOG_PATH": "/tmp/tenx/",
"outputOffload": "true",
"symbolMessageHashField": "tenx_hash",
"log10xMetricsEnabled": "false"
}
}
aws lambda update-function-configuration --function-name <fn> \
--layers <tenx-receive-layer-arn> <otel-collector-layer-arn> \
--environment file://env.json
TENX_LOG_PATH matters: Lambda's filesystem is read-only outside /tmp, and a log appender that cannot create its directory poisons the pipeline launch.
Step 4: Splice the Collector Config
Merge the loopback pairing into the collector config the functions already run. Existing receivers, processors, and the destination exporter stay untouched:
receivers:
# Return path from the engine extension (loopback, same sandbox)
otlp/tenx:
protocols:
grpc:
endpoint: 127.0.0.1:24225
exporters:
# Hand-off to the engine extension
otlp/tenx:
endpoint: 127.0.0.1:4317
tls:
insecure: true
# The collector's first dial happens before the engine listens, and
# default exponential retry intervals stretch across freeze until
# exports fail forever. Bounded intervals keep retries inside a
# thaw window.
retry_on_failure:
enabled: true
initial_interval: 200ms
max_interval: 1s
max_elapsed_time: 0s
connectors:
routing/tenx:
default_pipelines: [logs/tenx-siem]
table:
# context: log is required -- routeState is a LOG attribute; the
# resource context never matches it.
- context: log
condition: attributes["routeState"] == "offload"
pipelines: [logs/tenx-offload]
- context: log
condition: attributes["routeState"] == "drop"
pipelines: [logs/tenx-drop]
The full config, including the offload pipeline, the decouple processor placement, and the destination-side storage-tier policy, is emitted per estate by Advise.
Step 5: Per-Pattern Dispositions (Optional)
Without a mute file every lever is estate-wide. With one, each pattern gets its own disposition. Entries are <pattern>,<rate>:<untilEpochSec>:<reason>; rate 0 is a full mute, 0.25 keeps 25%, and past the epoch the entry expires on its own. The first line is a header row the engine's lookup consumes as column names:
pattern,disposition
heartbeat_check_ok,0:4102444800:liveness spam OPS-4821
jwt_validated,0.25:4102444800:auth flood after deploy
4102444800 is 2100-01-01, a placeholder — set a real expiry. A past
untilEpochSec is not an error: the file loads, the log says so, and
the entry mutes nothing.
Upload it to your bucket and add three variables to env.json:
TENX_RECEIVE_APPS=@run/input/forwarder/otel-collector,@apps/receiver,@run/receive/rate
rateReceiverFieldNames=message_pattern
rateReceiverLookupFile=/tmp/tenx/mutes.csv
TENX_RECEIVE_MUTE_S3_URI=s3://<your-bucket>/<prefix>/pipelines/run/receive/rate/mutes.csv
The extension fetches the file at INIT and refreshes on invoke (TENX_RECEIVE_MUTE_REFRESH_MS, default 5 minutes), so changing a disposition is one aws s3 cp with no redeploy. The function role needs s3:GetObject on that key. Alternatively, bake the file into the layer with build-receive-layer.sh's sixth argument; the policy is then static and a change means republishing the layer.
For a recurring loop that recomputes dispositions from cost data on a schedule, Setup recurring with scheduler: "eventbridge" emits an EventBridge Scheduler + CodeBuild stack that writes this same S3 key.
Step 6: Verify
The extension logs its lifecycle to the function's log group:
aws logs filter-log-events --log-group-name /aws/lambda/<fn> \
--filter-pattern "tenx-receive" --max-items 10
At the destination, processed records carry tenx_hash and routeState as log attributes; a muted pattern shows routeState of drop on all but its retention floor. If dispositions have no effect, check that the mute file's first line is the header row and that rateReceiverFieldNames is message_pattern.
Build the layer yourself (optional)
The published layer suits every case except one: baking a static mute file into the image, which pins the policy to the layer version. Build it from the release artifacts:
gh release download 1.1.69 -R log-10x/pipeline-releases \
-p 'tenx-edge-1.1.69-amd64-native' -p 'tenx-modules-1.1.69.tar.gz' \
-p 'tenx-config-1.1.69.tar.gz' -p 'tenx-symbols-1.1.69.10x.tar' \
-p 'build-receive-layer.sh'
mkdir modules config symbols
tar xzf tenx-modules-1.1.69.tar.gz -C modules
tar xzf tenx-config-1.1.69.tar.gz -C config
cp tenx-symbols-1.1.69.10x.tar symbols/
chmod +x build-receive-layer.sh
./build-receive-layer.sh \
./tenx-edge-1.1.69-amd64-native ./modules ./config ./symbols \
./tenx-receive.zip \
./mutes.csv # the 6th argument is the policy file
# The zip is ~90 MB; direct upload caps at ~70 MB, so stage through S3
aws s3 cp tenx-receive.zip s3://<your-bucket>/layers/tenx-receive.zip
aws lambda publish-layer-version --layer-name tenx-receive \
--compatible-architectures x86_64 \
--content S3Bucket=<your-bucket>,S3Key=layers/tenx-receive.zip
Swap the bootstrap for tenx-edge-1.1.69-aarch64-native and the architecture for arm64 to build the Graviton variant. With a mute file present the build validates every entry and refuses a malformed one, rather than shipping a disposition that would be silently skipped.