Skip to content

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.

env.json
{
  "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.