Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ docker compose -f docker/docker-compose.telemetry-generator.yml up -d
| `blitz-hostmetrics-windows` | Host Metrics | Synthetic host metrics (CPU, memory, disk, etc.) for Windows |
| `blitz-traces` | Traces | Synthetic distributed traces (HTTP + DB spans) |
| `blitz-fix` | FIX | FIX protocol messages (4.2 / 4.4 / 5.0 SP2) across 10 asset categories — NewOrderSingle, ExecutionReport, cancel/replace/status |
| `blitz-f5` | F5 | Multi-product F5 syslog: BIG-IP LTM/ASM/AFM/APM/DNS/audit, NGINX-on-F5 Plus + App Protect, iRules, F5OS |

## Running Individual Generators

Expand Down
13 changes: 13 additions & 0 deletions docker/docker-compose.telemetry-generator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,19 @@ services:
BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector
BLITZ_OUTPUT_OTLPGRPC_PORT: "4317"

# Multi-product F5 log generator (BIG-IP LTM/ASM/AFM/APM/DNS/audit,
# NGINX-on-F5 Plus + App Protect, iRules, F5OS). All products enabled
# by default; set BLITZ_GENERATOR_F5_ENABLEDPRODUCTS to a subset.
blitz-f5:
<<: *blitz-common
environment:
BLITZ_GENERATOR_TYPE: f5
BLITZ_GENERATOR_F5_WORKERS: ${BLITZ_WORKERS:-1}
BLITZ_GENERATOR_F5_RATE: ${BLITZ_RATE:-1s}
BLITZ_OUTPUT_TYPE: otlp-grpc
BLITZ_OUTPUT_OTLPGRPC_HOST: bdot-collector
BLITZ_OUTPUT_OTLPGRPC_PORT: "4317"

networks:
telemetry-net:
driver: bridge
Expand Down
122 changes: 122 additions & 0 deletions docs/generator/f5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# F5 Generator

The F5 generator emits a weighted mix of syslog-shaped log lines across the F5 product portfolio. It is a single generator (`generator/f5`) with one subpackage per product, mirroring the FIX generator: adding a product later is a matter of a new subpackage that self-registers into the catalog.

All products are enabled by default. Restrict the set with `enabledProducts`, and shift the mix with `weights`. Output is deterministic from `seed` (negative = randomize per worker; 0+ = byte-identical across runs).

## Products

| Token | Product | Shape |
|-------|---------|-------|
| `ltm` | BIG-IP LTM (Local Traffic Manager) | TMM request log, combined-style with virtual-server/pool context |
| `asm` | BIG-IP ASM (Application Security Manager) | WAF security event, comma-separated `key="value"` fields |
| `afm` | BIG-IP AFM (Advanced Firewall Manager) | L3/L4 firewall event, `key="value"` fields |
| `apm` | BIG-IP APM (Access Policy Manager) | Access / auth / SSO log with MCP-style code + session |
| `dns` | BIG-IP DNS (formerly GTM) | GSLB / DNS query-resolution log |
| `audit` | BIG-IP audit | `mcpd` configuration audit record (`AUDIT -` format) |
| `nginx-plus` | NGINX-on-F5 (NGINX Plus) | Access log (combined + Plus upstream fields) and error log |
| `nginx-app-protect` | NGINX App Protect (WAF) | Security event, `key="value"` fields |
| `irules` | iRules logging | Operator log line (`Rule /Common/<rule> <EVENT>:`) |
| `f5os` | F5OS / TMOS platform | Platform audit + system events (chassis, tenant, service) |

## Field fidelity and references

F5 logging is **operator-configurable**: the field set and order are chosen by the administrator (BIG-IP logging profiles / storage-format Field-List, NGINX `log_format`, iRules `log` statements). So the fidelity target is F5's **documented default format** for each product. Products with a published, enumerable default are **byte-exact to that default** (field set + order asserted by test). Products whose default is a free-text or fully operator-defined template are a **realistic instance of the documented default profile** (config-dependent, cited).

| Product | Fidelity | Field count | Reference URL |
|---------|----------|-------------|---------------|
| `nginx-app-protect` | byte-exact to default | 41 | https://docs.nginx.com/waf/logging/security-logs/ |
| `asm` | byte-exact to documented default syslog set | 16 | https://techdocs.f5.com/en-us/bigip-17-5-0/big-ip-asm-implementations/logging-application-security-events.html |
| `afm` | byte-exact to default ("None") format | 14 | https://techdocs.f5.com/kb/en-us/products/big-ip-afm/manuals/product/network-firewall-policies-implementations-11-6-0/13.html |
| `nginx-plus` | byte-exact to combined + Plus upstream vars | 8 + 5 | https://docs.nginx.com/nginx/admin-guide/monitoring/logging/ |
| `audit` | byte-exact to documented `mcpd` AUDIT template | template | https://techdocs.f5.com/en-us/bigip-17-5-0/external-monitoring-of-big-ip-systems-implementations.html |
| `ltm` | default-instance (config-dependent) | operator-defined | https://techdocs.f5.com/en-us/bigip-17-5-0/big-ip-ltm-implementations/configuring-request-logging.html |
| `apm` | default-instance (config-dependent) | operator-defined | https://techdocs.f5.com/en-us/bigip-17-5-0/big-ip-access-policy-manager-visual-policy-editor.html |
| `dns` | default-instance (config-dependent) | operator-defined | https://techdocs.f5.com/en-us/bigip-17-5-0/external-monitoring-of-big-ip-systems-implementations.html |
| `irules` | default-instance (config-dependent) | operator-defined | https://clouddocs.f5.com/api/irules/log.html |
| `f5os` | default-instance (config-dependent) | operator-defined | https://techdocs.f5.com/en-us/f5os-a-1-8-0/f5-rseries-systems-administration-configuration.html |

The five byte-exact products declare their default field order in code (`DefaultFields()` / `AccessFormatVars()`) and assert the emitted field set/order against it in tests. The five default-instance products emit a realistic instance of F5's documented default profile and are marked config-dependent in their package docs, since F5 publishes no fixed positional spec for them.

## Configuration

YAML (standalone CLI):

```yaml
generator:
type: f5
f5:
workers: 2
rate: 500ms
hostname: bigip-prod-01 # syslog-header device hostname
enabledProducts: # omit / leave empty for all 10
- ltm
- asm
- afm
weights: # omit for an equal mix
asm: 5
ltm: 2
seed: 42 # >=0 deterministic, <0 randomize
```

Environment variables map to these paths with the `BLITZ_` prefix (e.g. `BLITZ_GENERATOR_F5_WORKERS`, `BLITZ_GENERATOR_F5_RATE`), as used by the docker compose service.

Programmatic (`f5.Config` in Go):

```go
type Config struct {
Workers int // parallel emission workers
Rate time.Duration // 1 line per Rate per worker
Hostname string // syslog-header device hostname
EnabledProducts []string // empty = all 10 products
Weights map[string]float64 // per-product mix ratio; absent = 1
Seed int64 // >=0 deterministic, <0 randomize
}
```

## Example Configuration

### All products, equal mix

```yaml
generator:
type: f5
f5:
workers: 2
rate: 500ms
hostname: bigip-prod-01
```

### WAF-focused subset with a weighted mix

```yaml
generator:
type: f5
f5:
enabledProducts: [asm, nginx-app-protect, afm]
weights:
asm: 5
nginx-app-protect: 3
afm: 1
seed: 42
```

## Example Output

LTM request log:

```
<134>Sep 24 15:04:05 bigip1 tmm[4211]: 203.0.113.7 - - [24/Sep/2026:15:04:05 +0000] "GET /api/v1/orders HTTP/1.1" 200 8231 "-" "curl/8.4.0" vs=/Common/vs_https_443 pool=/Common/pool_web member=10.2.3.4:8080
```

ASM security event (truncated):

```
<134>Sep 24 15:04:05 bigip1 ASM: unit_hostname="bigip1",policy_name="/Common/prod_waf_policy",violations="SQL-Injection",support_id="1234567890123456",request_status="blocked",method="POST",attack_type="SQL-Injection",severity="Critical",...
```

Audit record:

```
<133>Sep 24 15:04:05 bigip1 mcpd[4102]: 01070417:5: AUDIT - client tmsh, user admin - transaction #4211-1 - object 0 - modify ltm virtual /Common/vs_https_443 { ... } from 10.1.1.1
```
29 changes: 29 additions & 0 deletions generator/f5/catalog/product.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Package catalog holds the F5 product registry. Each per-product
// subpackage under generator/f5/products registers one Product at init
// time via Register; the top-level f5 generator reads them back to emit
// a weighted mix of F5 log lines. Adding a product is a matter of a new
// subpackage that self-registers — no change to the generator core.
package catalog

import (
"math/rand"
"time"
)

// Ctx carries the per-line context a product's Build func needs: the
// emission time and the emitting device hostname. All randomness comes
// from the supplied *rand.Rand so output is deterministic from seed.
type Ctx struct {
// Now is the timestamp for the emitted record.
Now time.Time
// Hostname is the simulated F5 device hostname.
Hostname string
}

// Product is one F5 product's log emitter. Name is the config token
// (e.g. "ltm", "asm"). Build returns one syslog-shaped log line for the
// product, drawing all randomness from r.
type Product struct {
Name string
Build func(r *rand.Rand, c *Ctx) string
}
54 changes: 54 additions & 0 deletions generator/f5/catalog/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package catalog

import (
"sort"
"sync"
)

// registry holds all registered Products keyed by Name. Per-product
// subpackages register at init (single-goroutine); the generator reads
// them on construction. Guarded by an RWMutex for safety.
var (
mu sync.RWMutex
products = map[string]Product{}
)

// Register adds a Product to the global registry. Panics on a duplicate
// Name — that signals a programmer error in the product catalog, not a
// recoverable runtime condition.
func Register(p Product) {
mu.Lock()
defer mu.Unlock()
if _, exists := products[p.Name]; exists {
panic("f5 catalog: duplicate product registration for " + p.Name)
}
products[p.Name] = p
}

// Get returns the Product registered under name, or ok=false.
func Get(name string) (Product, bool) {
mu.RLock()
defer mu.RUnlock()
p, ok := products[name]
return p, ok
}

// AllProducts returns every registered Product, sorted by Name for
// deterministic ordering.
func AllProducts() []Product {
mu.RLock()
defer mu.RUnlock()
out := make([]Product, 0, len(products))
for _, p := range products {
out = append(out, p)
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}

// ResetForTest empties the registry. Tests only.
func ResetForTest() {
mu.Lock()
defer mu.Unlock()
products = map[string]Product{}
}
44 changes: 44 additions & 0 deletions generator/f5/catalog/registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package catalog

import (
"math/rand"
"testing"

"github.com/stretchr/testify/require"
)

func TestRegisterAndGet(t *testing.T) {
ResetForTest()
p := Product{Name: "ltm", Build: func(_ *rand.Rand, _ *Ctx) string { return "line" }}
Register(p)

got, ok := Get("ltm")
require.True(t, ok)
require.Equal(t, "ltm", got.Name)
require.Equal(t, "line", got.Build(rand.New(rand.NewSource(1)), &Ctx{}))
}

func TestRegisterDuplicatePanics(t *testing.T) {
ResetForTest()
Register(Product{Name: "ltm", Build: func(_ *rand.Rand, _ *Ctx) string { return "" }})
require.Panics(t, func() {
Register(Product{Name: "ltm", Build: func(_ *rand.Rand, _ *Ctx) string { return "" }})
})
}

func TestAllProductsSortedByName(t *testing.T) {
ResetForTest()
Register(Product{Name: "zzz", Build: func(_ *rand.Rand, _ *Ctx) string { return "" }})
Register(Product{Name: "aaa", Build: func(_ *rand.Rand, _ *Ctx) string { return "" }})

all := AllProducts()
require.Len(t, all, 2)
require.Equal(t, "aaa", all[0].Name)
require.Equal(t, "zzz", all[1].Name)
}

func TestGetMissing(t *testing.T) {
ResetForTest()
_, ok := Get("nope")
require.False(t, ok)
}
20 changes: 20 additions & 0 deletions generator/f5/catalog/syslog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package catalog

import (
"fmt"
"time"
)

// bsdStamp is the BSD-syslog (RFC 3164) timestamp layout: month, space-
// padded day, time. F5 devices emit this on the syslog wire.
const bsdStamp = "Jan _2 15:04:05"

// SyslogHeader builds an RFC 3164 header: "<pri>timestamp host tag[pid]:".
// A pid <= 0 omits the "[pid]" segment (used by daemons like mcpd that
// log without one in some records).
func SyslogHeader(pri int, now time.Time, host, tag string, pid int) string {
if pid <= 0 {
return fmt.Sprintf("<%d>%s %s %s:", pri, now.Format(bsdStamp), host, tag)
}
return fmt.Sprintf("<%d>%s %s %s[%d]:", pri, now.Format(bsdStamp), host, tag, pid)
}
20 changes: 20 additions & 0 deletions generator/f5/catalog/syslog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package catalog

import (
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestSyslogHeader(t *testing.T) {
now := time.Date(2026, 9, 24, 15, 4, 5, 0, time.UTC)
got := SyslogHeader(134, now, "bigip1", "tmm", 4211)
require.Equal(t, "<134>Sep 24 15:04:05 bigip1 tmm[4211]:", got)
}

func TestSyslogHeaderNoPID(t *testing.T) {
now := time.Date(2026, 9, 24, 15, 4, 5, 0, time.UTC)
got := SyslogHeader(134, now, "bigip1", "mcpd", 0)
require.Equal(t, "<134>Sep 24 15:04:05 bigip1 mcpd:", got)
}
Loading
Loading