This is an automated email from the ASF dual-hosted git repository. fmariani pushed a commit to branch smart-log-analyzer in repository https://gitbox.apache.org/repos/asf/camel-jbang-examples.git
commit 3129d478323f3bf6a0aacd33e77ea92424a21e57 Author: Croway <[email protected]> AuthorDate: Wed Jan 28 13:14:18 2026 +0100 Smart log analyzer example --- .gitignore | 1 + smart-log-analyzer/README.adoc | 215 +++++++++++++ .../analyzer/application-dev.properties | 31 ++ .../analyzer/error-analyzer.camel.yaml | 74 +++++ .../caches/infinispan-events-config.json | 14 + .../infinispan-events-to-process-config.json | 14 + smart-log-analyzer/containers/docker-compose.yaml | 98 ++++++ .../containers/otel-collector-config.yaml | 32 ++ .../correlator/application-dev.properties | 28 ++ .../correlator/correlated-log-schema.json | 38 +++ .../correlator/correlated-trace-schema.json | 58 ++++ .../correlator/infinispan.camel.yaml | 104 +++++++ smart-log-analyzer/correlator/kafka-ca-cert.pem | 70 +++++ .../correlator/kaoto-datamapper-4a94acc3.xsl | 38 +++ .../correlator/kaoto-datamapper-8f5bb2dd.xsl | 64 ++++ .../correlator/logs-mapper.camel.yaml | 45 +++ .../correlator/otel-log-record-schema.json | 137 +++++++++ .../correlator/otel-logs-schema.json | 227 ++++++++++++++ .../correlator/otel-span-schema.json | 244 +++++++++++++++ .../correlator/otel-traces-schema.json | 334 +++++++++++++++++++++ .../correlator/traces-mapper.camel.yaml | 45 +++ .../first-iteration/analyzer.camel.yaml | 62 ++++ .../first-iteration/application.properties | 2 + .../first-iteration/load-generator.camel.yaml | 48 +++ .../test/first-iteration.camel.it.yaml | 46 +++ .../first-iteration/test/jbang.properties | 3 + .../first-iteration/test/payload-error.json | 1 + .../first-iteration/test/payload-info.json | 1 + smart-log-analyzer/log-generator/agent.properties | 6 + .../log-generator/application-dev.properties | 3 + .../log-generator/log-generator.camel.yaml | 87 ++++++ .../log-generator/opentelemetry-javaagent.jar | Bin 0 -> 24026362 bytes .../ui-console/application-dev.properties | 31 ++ smart-log-analyzer/ui-console/index.html | 287 ++++++++++++++++++ .../ui-console/jms-file-storage.camel.yaml | 14 + smart-log-analyzer/ui-console/rest-api.camel.yaml | 84 ++++++ 36 files changed, 2586 insertions(+) diff --git a/.gitignore b/.gitignore index aeaba61..7bda6c8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ target/ camel-runner.jar *.i?? .citrus-jbang/ +.kaoto \ No newline at end of file diff --git a/smart-log-analyzer/README.adoc b/smart-log-analyzer/README.adoc new file mode 100644 index 0000000..c96d25b --- /dev/null +++ b/smart-log-analyzer/README.adoc @@ -0,0 +1,215 @@ += Smart Log Analyzer + +An intelligent observability system that correlates OpenTelemetry logs and traces, then uses LLM for automated root cause analysis. + +== Architecture + +[source,mermaid] +---- +sequenceDiagram + participant App as Application + participant OTel as OTEL Collector + participant Kafka as Kafka + participant Correlator as Correlator + participant Events as Infinispan (events) + participant ToProcess as Infinispan (events-to-process) + participant JMS as JMS Queue + participant Analyzer as Analyzer + participant LLM as Ollama LLM + participant UIConsole as UI Console + + App->>OTel: Send traces & logs (OTLP) + OTel->>Kafka: Export to otlp_spans & otlp_logs topics + + Kafka->>Correlator: Consume spans + Kafka->>Correlator: Consume logs + Correlator->>Correlator: Transform via XSLT + Correlator->>Events: Store by traceId (TTL: 5min) + + alt Error detected (severityText = ERROR) + Correlator->>ToProcess: Add traceId (TTL: 20s) + Note over ToProcess: Wait for related events + ToProcess-->>Correlator: Cache entry expired event + Correlator->>JMS: Send traceId to queue + JMS->>Analyzer: Consume traceId + Analyzer->>Events: Fetch all events for traceId + Analyzer->>LLM: Send events for analysis + LLM-->>Analyzer: Root cause analysis result + Analyzer->>JMS: Send analysis result + JMS->>UIConsole: Consume analysis result + UIConsole->>UIConsole: Save and display result + end +---- + +== First Iteration (Prototype) + +The `first-iteration/` folder contains a simpler prototype using in-memory aggregation. Ideal for learning and local testing, but won't work in distributed deployments. To run it: + +[source,bash] +---- +# Terminal 1: Start Kafka +camel infra run kafka + +# Terminal 2: Start the load generator +cd first-iteration +camel run load-generator.camel.yaml + +# Terminal 3: Start the analyzer (requires Ollama with granite4:3b) +cd first-iteration +camel run analyzer.camel.yaml --dep=org.apache.camel:camel-openai +---- + +=== Running the Test + +The first-iteration includes a Citrus integration test that uses `--stub=openai` to mock the LLM endpoint. + +First, install Citrus as a JBang app (one-time setup): + +[source,bash] +---- +jbang app install citrus@citrusframework/citrus +---- + +Then run the test: + +[source,bash] +---- +cd first-iteration/test +citrus run first-iteration.camel.it.yaml +---- + +The test starts Kafka via testcontainers, sends log events, and verifies the aggregation triggers correctly. + +== Components + +=== Correlator + +The correlator is the central event processing component: + +* Consumes OTEL logs from `otlp_logs` Kafka topic +* Consumes OTEL traces from `otlp_spans` Kafka topic +* Transforms complex OTEL format to simplified JSON via XSLT +* Correlates all events by traceId in Infinispan `events` cache (TTL: 5min) +* Uses `events-to-process` cache for deferred error analysis: +** When an ERROR log is detected, adds the traceId to this cache with a 20s TTL +** Listens for cache expiration events to trigger analysis +** This delay ensures all related spans and logs are collected before analysis +* Sends expired traceIds to JMS queue for the Analyzer + +=== Analyzer + +The analyzer performs intelligent root cause analysis: + +* Listens on JMS `error-logs` queue for traceIds from the Correlator +* Retrieves full trace context from Infinispan `events` cache +* Sends events to Ollama LLM (granite4:3b) for intelligent analysis +* Outputs analysis results to logs + +=== Log Generator + +A synthetic log generator for demonstration purposes that shows how to add OpenTelemetry observability to Apache Camel applications. It simulates order processing with intentional errors (30% failure rate). + +*Adding OpenTelemetry to your Camel application requires:* + +. *opentelemetry-javaagent.jar* - The OTEL Java agent for automatic instrumentation. Download from https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases[OpenTelemetry releases]. + +. *agent.properties* - Configures the OTEL agent: ++ +[source,properties] +---- +otel.service.name=log-generator +otel.traces.exporter=otlp +otel.logs.exporter=otlp +otel.metrics.exporter=none +---- + +. *application-dev.properties* - Enables Camel OpenTelemetry integration: ++ +[source,properties] +---- +camel.opentelemetry2.enabled = true +camel.jbang.dependencies=org.apache.camel:camel-opentelemetry2 +---- + +Run with the javaagent flag pointing to the agent jar and its configuration file. With this setup, Camel will automatically expose custom traces for route executions and exchanges. + +=== UI Console + +The UI Console provides visualization and storage for analysis results: + +* Listens on JMS queue for analysis results from the Analyzer +* Stores analysis results to files for persistence +* Exposes REST API to display and access the results +* Provides a user interface to view root cause analysis + +== Prerequisites + +* Docker & Docker Compose +* https://www.jbang.dev/download/[JBang] +* https://ollama.ai/[Ollama] with `granite4:3b` model: ++ +[source,bash] +---- +ollama pull granite4:3b +---- + +== Running the Application + +=== 1. Start Infrastructure + +[source,bash] +---- +cd containers +docker-compose up +---- + +=== 2. Start Correlator + +[source,bash] +---- +cd correlator +camel run \ + traces-mapper.camel.yaml \ + logs-mapper.camel.yaml \ + infinispan.camel.yaml \ + kaoto-datamapper-4a94acc3.xsl \ + kaoto-datamapper-8f5bb2dd.xsl +---- + +=== 3. Start Analyzer + +[source,bash] +---- +cd analyzer +camel run error-analyzer.camel.yaml +---- + +=== 4. Start Log Generator + +[source,bash] +---- +cd log-generator +jbang --javaagent=./opentelemetry-javaagent.jar \ + -Dotel.javaagent.configuration-file=./agent.properties \ + camel@apache/camel run log-generator.camel.yaml +---- + +=== 5. Start UI Console + +[source,bash] +---- +cd ui-console +camel run * +---- + +== Infrastructure Services + +[cols="1,1,2"] +|=== +| Service | Port | Purpose + +| Kafka | 9092 | Message broker for OTEL data +| Infinispan | 11222 | Event cache (admin:password) +| OTEL Collector | 4317, 4318 | Telemetry receiver (gRPC, HTTP) +| ActiveMQ Artemis | 61616, 8161 | JMS broker (artemis:artemis) +|=== diff --git a/smart-log-analyzer/analyzer/application-dev.properties b/smart-log-analyzer/analyzer/application-dev.properties new file mode 100644 index 0000000..caef75f --- /dev/null +++ b/smart-log-analyzer/analyzer/application-dev.properties @@ -0,0 +1,31 @@ +# Infinispan configuration +camel.component.infinispan.hosts=localhost:11222 +camel.component.infinispan.username=admin +camel.component.infinispan.password=password +camel.component.infinispan.sasl-mechanism=DIGEST-MD5 +camel.component.infinispan.security-realm=default +camel.component.infinispan.security-server-name=infinispan +camel.component.infinispan.secure=true + +# Artemis JMS configuration +camel.beans.artemisCF = #class:org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory +camel.beans.artemisCF.brokerURL = tcp://localhost:61616 +camel.beans.artemisCF.user = artemis +camel.beans.artemisCF.password = artemis +camel.beans.poolCF = #class:org.messaginghub.pooled.jms.JmsPoolConnectionFactory +camel.beans.poolCF.connectionFactory = #bean:artemisCF +camel.beans.poolCF.maxSessionsPerConnection = 500 +camel.beans.poolCF.connectionIdleTimeout = 20000 +camel.component.jms.connection-factory = #bean:poolCF +camel.jms.queue.error-logs=error-logs +camel.jms.queue.analysis-result=analysis-result + +# Ollama configuration (OpenAI-compatible API) +camel.component.openai.apiKey=ollama +camel.component.openai.baseUrl=http://localhost:11434/v1 +camel.component.openai.model=granite4:3b + +# Camel configuration +camel.main.name=analyzer + +camel.jbang.dependencies=org.apache.camel:camel-openai diff --git a/smart-log-analyzer/analyzer/error-analyzer.camel.yaml b/smart-log-analyzer/analyzer/error-analyzer.camel.yaml new file mode 100644 index 0000000..406f570 --- /dev/null +++ b/smart-log-analyzer/analyzer/error-analyzer.camel.yaml @@ -0,0 +1,74 @@ +# Analyzes error logs by retrieving OpenTelemetry events from Infinispan +# and sending them to an LLM for root cause analysis and recommendations +- route: + id: error-log-analyzer + from: + uri: jms:{{camel.jms.queue.error-logs}} + steps: + - log: + loggingLevel: INFO + message: Received ERROR log for analysis + - setVariable: + name: traceId + simple: ${body} + - log: + loggingLevel: INFO + message: "Retrieving all events for traceId: ${variable.traceId}" + - setHeader: + name: CamelInfinispanKey + simple: otel-${variable.traceId} + - to: + uri: infinispan:events + parameters: + operation: GET + - choice: + otherwise: + steps: + - log: + loggingLevel: WARN + message: "No events found in Infinispan for traceId: ${variable.traceId}" + when: + - steps: + - setVariable: + name: recordCount + jq: + expression: length + resultType: java.lang.String + - log: + message: Sending ${variable.recordCount} records to LLM for analysis + - setVariable: + name: eventsJson + simple: ${body} + - setBody: + simple: > + Analyze the following OpenTelemetry logs and traces for + traceId ${variable.traceId}. + + Identify the root cause of the error, explain what + happened, and suggest possible fixes. + + + Events (sorted by timestamp): + + ${variable.eventsJson} + - to: + uri: openai:chat-completion + parameters: + systemMessage: > + You are an expert DevOps engineer and log analyst. + Analyze OpenTelemetry logs and traces to identify + errors, their root causes, and provide actionable + recommendations. Be concise and focus on the error. + You want to build a tree of calls by associating each + span with its parent, where parentSpanId corresponds + to the parent caller’s spanId. + - log: + message: "LLM Analysis for traceId ${variable.traceId}: ${body}" + - setHeader: + name: traceId + simple: "${variable.traceId}" + - to: + uri: jms:{{camel.jms.queue.analysis-result}} + - log: + message: "Sent analysis result to queue for traceId: ${variable.traceId}" + simple: ${body} != null && ${body} != '' diff --git a/smart-log-analyzer/containers/caches/infinispan-events-config.json b/smart-log-analyzer/containers/caches/infinispan-events-config.json new file mode 100644 index 0000000..da20578 --- /dev/null +++ b/smart-log-analyzer/containers/caches/infinispan-events-config.json @@ -0,0 +1,14 @@ +{ + "events": { + "distributed-cache": { + "mode": "SYNC", + "statistics": true, + "encoding": { + "media-type": "text/plain" + }, + "expiration": { + "lifespan": "600000ms" + } + } + } +} diff --git a/smart-log-analyzer/containers/caches/infinispan-events-to-process-config.json b/smart-log-analyzer/containers/caches/infinispan-events-to-process-config.json new file mode 100644 index 0000000..d34d9a4 --- /dev/null +++ b/smart-log-analyzer/containers/caches/infinispan-events-to-process-config.json @@ -0,0 +1,14 @@ +{ + "events-to-process": { + "distributed-cache": { + "mode": "SYNC", + "statistics": true, + "encoding": { + "media-type": "text/plain" + }, + "expiration": { + "lifespan": "20000ms" + } + } + } +} diff --git a/smart-log-analyzer/containers/docker-compose.yaml b/smart-log-analyzer/containers/docker-compose.yaml new file mode 100644 index 0000000..afc84d9 --- /dev/null +++ b/smart-log-analyzer/containers/docker-compose.yaml @@ -0,0 +1,98 @@ +services: + kafka: + image: apache/kafka:latest + container_name: kafka + hostname: kafka + environment: + # KRaft mode configuration (no Zookeeper) + KAFKA_NODE_ID: 1 + KAFKA_PROCESS_ROLES: broker,controller + KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 + # Multiple listeners: PLAINTEXT for Docker network, PLAINTEXT_HOST for localhost access + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:19092,PLAINTEXT_HOST://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:19092,PLAINTEXT_HOST://localhost:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,CONTROLLER:PLAINTEXT + KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_DEFAULT_REPLICATION_FACTOR: 1 + KAFKA_MIN_INSYNC_REPLICAS: 1 + # Storage (ephemeral - no persistence) + KAFKA_LOG_DIRS: /tmp/kraft-combined-logs + ports: + - "9092:9092" # External access from localhost + networks: + - otel-network + + infinispan: + image: quay.io/infinispan/server:16.0.5 + container_name: infinispan + hostname: infinispan + environment: + USER: admin + PASS: password + ports: + - "11222:11222" + networks: + - otel-network + healthcheck: + test: ["CMD", "curl", "-u", "admin:password", "--digest", "http://localhost:11222/rest/v2/caches"] + interval: 5s + timeout: 3s + retries: 8 + + infinispan-init: + image: curlimages/curl:latest + container_name: infinispan-init + depends_on: + infinispan: + condition: service_healthy + networks: + - otel-network + volumes: + - ./caches:/etc/caches:ro + command: > + sh -c " + sleep 2 && + echo 'Creating caches...' && + curl -f -u admin:password --digest -X POST 'http://infinispan:11222/rest/v2/caches/events' -H 'Content-Type: application/json' -d @/etc/caches/infinispan-events-config.json && + curl -f -u admin:password --digest -X POST 'http://infinispan:11222/rest/v2/caches/events-to-process' -H 'Content-Type: application/json' -d @/etc/caches/infinispan-events-to-process-config.json && + echo 'Caches created successfully' + " + deploy: + restart_policy: + condition: on-failure + max_attempts: 5 + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + container_name: otel-collector + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml + ports: + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + networks: + - otel-network + depends_on: + - kafka + + artemis: + image: apache/activemq-artemis:latest + container_name: artemis + hostname: artemis + environment: + ARTEMIS_USER: artemis + ARTEMIS_PASSWORD: artemis + ports: + - "8161:8161" # Web console + - "61616:61616" # Core protocol + networks: + - otel-network + +networks: + otel-network: + driver: bridge diff --git a/smart-log-analyzer/containers/otel-collector-config.yaml b/smart-log-analyzer/containers/otel-collector-config.yaml new file mode 100644 index 0000000..876bbf6 --- /dev/null +++ b/smart-log-analyzer/containers/otel-collector-config.yaml @@ -0,0 +1,32 @@ +exporters: + debug: + verbosity: detailed + kafka: + brokers: + - kafka:19092 + logs: + encoding: otlp_json + traces: + encoding: otlp_json +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +service: + pipelines: + logs: + receivers: + - otlp + exporters: + - debug + - kafka + traces: + receivers: + - otlp + exporters: + - debug + - kafka diff --git a/smart-log-analyzer/correlator/application-dev.properties b/smart-log-analyzer/correlator/application-dev.properties new file mode 100644 index 0000000..3f126e6 --- /dev/null +++ b/smart-log-analyzer/correlator/application-dev.properties @@ -0,0 +1,28 @@ +# Kafka configuration +camel.component.kafka.brokers=localhost:9092 +camel.kafka.topic.logs=otlp_logs +camel.kafka.topic.spans=otlp_spans + +# Infinispan configuration +camel.component.infinispan.hosts=localhost:11222 +camel.component.infinispan.username=admin +camel.component.infinispan.password=password +camel.component.infinispan.sasl-mechanism=DIGEST-MD5 +camel.component.infinispan.security-realm=default +camel.component.infinispan.security-server-name=infinispan +camel.component.infinispan.secure=true + +# Artemis JMS configuration +camel.beans.artemisCF = #class:org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory +camel.beans.artemisCF.brokerURL = tcp://localhost:61616 +camel.beans.artemisCF.user = artemis +camel.beans.artemisCF.password = artemis +camel.beans.poolCF = #class:org.messaginghub.pooled.jms.JmsPoolConnectionFactory +camel.beans.poolCF.connectionFactory = #bean:artemisCF +camel.beans.poolCF.maxSessionsPerConnection = 500 +camel.beans.poolCF.connectionIdleTimeout = 20000 +camel.component.jms.connection-factory = #bean:poolCF +camel.jms.queue.error-logs=error-logs + +# Camel configuration +camel.main.name=correlator diff --git a/smart-log-analyzer/correlator/correlated-log-schema.json b/smart-log-analyzer/correlator/correlated-log-schema.json new file mode 100644 index 0000000..a49f953 --- /dev/null +++ b/smart-log-analyzer/correlator/correlated-log-schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "correlated-log-entry", + "title": "Correlated Log Entry", + "description": "Simplified log entry for storage in Infinispan", + "type": "object", + "required": ["timeUnixNano", "severityText", "message", "traceId", "spanId"], + "properties": { + "timeUnixNano": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Time when the event occurred (nanoseconds since Unix epoch)" + }, + "observedTimeUnixNano": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Time when the event was observed (nanoseconds since Unix epoch)" + }, + "severityText": { + "type": "string", + "description": "Severity level (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)" + }, + "message": { + "type": "string", + "description": "Combined message from body and attributes" + }, + "traceId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{32}$", + "description": "Trace ID for correlation" + }, + "spanId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{16}$", + "description": "Span ID" + } + } +} diff --git a/smart-log-analyzer/correlator/correlated-trace-schema.json b/smart-log-analyzer/correlator/correlated-trace-schema.json new file mode 100644 index 0000000..7cdec55 --- /dev/null +++ b/smart-log-analyzer/correlator/correlated-trace-schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "correlated-trace-entry", + "title": "Correlated Trace Entry", + "description": "Simplified span entry for storage in Infinispan, designed for LLM error analysis", + "type": "object", + "required": ["type", "timeUnixNano", "traceId", "spanId", "name", "status"], + "properties": { + "type": { + "type": "string", + "const": "span", + "description": "Entry type to distinguish from logs in combined timeline" + }, + "timeUnixNano": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Start time of the span (nanoseconds since Unix epoch), used for ordering with logs" + }, + "durationNano": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Duration of the span in nanoseconds (endTimeUnixNano - startTimeUnixNano)" + }, + "traceId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{32}$", + "description": "Trace ID for correlation with logs" + }, + "spanId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{16}$", + "description": "Span ID" + }, + "parentSpanId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{16}$", + "description": "Parent span ID for understanding call hierarchy" + }, + "name": { + "type": "string", + "description": "Operation name (e.g., timer, http.request)" + }, + "kind": { + "type": "string", + "enum": ["UNSPECIFIED", "INTERNAL", "SERVER", "CLIENT", "PRODUCER", "CONSUMER"], + "description": "Span kind indicating the type of operation" + }, + "status": { + "type": "string", + "enum": ["UNSET", "OK", "ERROR"], + "description": "Span status" + }, + "message": { + "type": "string", + "description": "Combined message from key attributes and error event messages" + } + } +} diff --git a/smart-log-analyzer/correlator/infinispan.camel.yaml b/smart-log-analyzer/correlator/infinispan.camel.yaml new file mode 100644 index 0000000..c4e0cde --- /dev/null +++ b/smart-log-analyzer/correlator/infinispan.camel.yaml @@ -0,0 +1,104 @@ +# Stores OpenTelemetry records in Infinispan cache, grouped by traceId +# and marks ERROR events for deferred processing +- route: + id: route-1200 + from: + id: from-9847 + uri: direct + parameters: + name: store + steps: + - log: + id: log-record + loggingLevel: DEBUG + message: "Processing record: ${body}" + - setVariable: + jq: + expression: .traceId + resultType: java.lang.String + name: traceId + - filter: + expression: + simple: ${variable.traceId} != null && ${variable.traceId} != '' + steps: + - claimCheck: + key: currentRecord + operation: Set + - setHeader: + name: CamelInfinispanKey + simple: otel-${variable.traceId} + - to: + uri: infinispan:events + parameters: + operation: GET + - choice: + otherwise: + steps: + - claimCheck: + key: currentRecord + operation: Get + - setBody: + jq: + expression: "[.]" + resultType: java.lang.String + when: + - steps: + - setVariable: + name: existingRecords + simple: ${body} + - claimCheck: + key: currentRecord + operation: Get + - setBody: + jq: + expression: (variable("existingRecords") | fromjson) + [.] | + sort_by(.timeUnixNano) + resultType: java.lang.String + simple: ${body} != null && ${body} != '' + - setHeader: + name: CamelInfinispanKey + simple: otel-${variable.traceId} + - setHeader: + name: CamelInfinispanValue + simple: ${body} + - to: + uri: infinispan:events + parameters: + operation: PUT + - claimCheck: + key: currentRecord + operation: GetAndRemove + - log: + id: log-stored + message: "Stored record for traceId: ${variable.traceId}" + - choice: + when: + - steps: + - log: + message: "Adding ERROR log to events-to-process cache for traceId: + ${variable.traceId}" + - setHeader: + name: CamelInfinispanKey + simple: ${variable.traceId} + - setHeader: + name: CamelInfinispanValue + simple: ${variable.traceId} + - to: + uri: infinispan:events-to-process + parameters: + operation: PUTIFABSENT + jq: + expression: .severityText == "ERROR" +- route: + id: route-expired-events + from: + uri: infinispan:events-to-process + parameters: + eventTypes: CLIENT_CACHE_ENTRY_EXPIRED + steps: + - setBody: + simple: ${header.CamelInfinispanKey} + - log: + message: "Expired cache entry received, sending to JMS queue: ${body}" + - to: + uri: jms:{{camel.jms.queue.error-logs}} diff --git a/smart-log-analyzer/correlator/kafka-ca-cert.pem b/smart-log-analyzer/correlator/kafka-ca-cert.pem new file mode 100644 index 0000000..3ea54c6 --- /dev/null +++ b/smart-log-analyzer/correlator/kafka-ca-cert.pem @@ -0,0 +1,70 @@ +-----BEGIN CERTIFICATE----- +MIIHFTCCBP2gAwIBAgIUddBSUC646dJUTeT+vKy9E8WclvAwDQYJKoZIhvcNAQEN +BQAwLTETMBEGA1UECgwKaW8uc3RyaW16aTEWMBQGA1UEAwwNY2x1c3Rlci1jYSB2 +MDAeFw0yNjAxMTYxNDI4NTZaFw0yNzAxMTYxNDI4NTZaMDMxEzARBgNVBAoMCmlv +LnN0cmltemkxHDAaBgNVBAMME2NhbWVsLWNsdXN0ZXIta2Fma2EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPWjgPFIDyK0i7bns4j28WZhD5xUgIol/0 +ns0awE6MZGFerQEOQXcbwrFmAn/LoTnAnlzcROOx096KJVNXj6JnaSBbGvrnCcYF +2d6pBz7WxfHG9Pn/I6qMYTgc3XiJ4KLO+CZ/GKzo9fc6dl5kkciVS61thq26jkV1 +4aTA6oz9RComXdFjlzuSBoXULrSijWRYrGaT618kSYE2MBfJjOdAv08WoLE5UVf8 +F+bLEInJ7j1Z+/fmCxj0UB8j2UH3Wh/oAaQUBP1IGOPYfcfhiRzKgy7RWTGKMOOc +pTmtHm5wRDvSm+1OYSZlFyMxtmmqK4ZstWf0OJhrORF2Kb6KJTTrAgMBAAGjggMl +MIIDITCCAt0GA1UdEQSCAtQwggLQgj5jYW1lbC1jbHVzdGVyLWthZmthLWJyb2tl +cnMuY2FtZWwtb3RlbC1pbmZyYS5zdmMuY2x1c3Rlci5sb2NhbIJYY2FtZWwtY2x1 +c3Rlci1tdWx0aXJvbGUtMC5jYW1lbC1jbHVzdGVyLWthZmthLWJyb2tlcnMuY2Ft +ZWwtb3RlbC1pbmZyYS5zdmMuY2x1c3Rlci5sb2NhbIIwY2FtZWwtY2x1c3Rlci1r +YWZrYS1icm9rZXJzLmNhbWVsLW90ZWwtaW5mcmEuc3Zjgh1jYW1lbC1jbHVzdGVy +LWthZmthLWJvb3RzdHJhcIJUY2FtZWwtY2x1c3Rlci1rYWZrYS1ib290c3RyYXAt +Y2FtZWwtb3RlbC1pbmZyYS5hcHBzLmNzYi1sYXN0LmZ1c2UuaW50ZWdyYXRpb24t +cWUuY29tgjJjYW1lbC1jbHVzdGVyLWthZmthLWJvb3RzdHJhcC5jYW1lbC1vdGVs +LWluZnJhLnN2Y4JKY2FtZWwtY2x1c3Rlci1tdWx0aXJvbGUtMC5jYW1lbC1jbHVz +dGVyLWthZmthLWJyb2tlcnMuY2FtZWwtb3RlbC1pbmZyYS5zdmOCQGNhbWVsLWNs +dXN0ZXIta2Fma2EtYm9vdHN0cmFwLmNhbWVsLW90ZWwtaW5mcmEuc3ZjLmNsdXN0 +ZXIubG9jYWyCLmNhbWVsLWNsdXN0ZXIta2Fma2EtYm9vdHN0cmFwLmNhbWVsLW90 +ZWwtaW5mcmGCLGNhbWVsLWNsdXN0ZXIta2Fma2EtYnJva2Vycy5jYW1lbC1vdGVs +LWluZnJhghtjYW1lbC1jbHVzdGVyLWthZmthLWJyb2tlcnOCUGNhbWVsLWNsdXN0 +ZXItbXVsdGlyb2xlLTAtY2FtZWwtb3RlbC1pbmZyYS5hcHBzLmNzYi1sYXN0LmZ1 +c2UuaW50ZWdyYXRpb24tcWUuY29tMB0GA1UdDgQWBBQTlA+mmTCgeDSUKAwSpkIB +TX+2+DAfBgNVHSMEGDAWgBTQeJU9RNTnrxXBxi8RaI3kQ6Y3RDANBgkqhkiG9w0B +AQ0FAAOCAgEAIpbu83u9leM4dMqOLJWX1ZYAR31DxkhI+jMOe1JLxatD7UV/m/sh +bdOaT1kV/ddynpuBxryXmrsm63kcCmUxYtI8bs/hLoxhXRAAy4CM9fezx7C5zVqW +rqJ8Xfy8tTlDyHW9Ar3mxffQN3Sgnuvb+yqeW+OGj3nnXosrPL6cNZ2s19uRMjdl +j5jfZu8tOT74UJvnQVuESfAxXvK0X/5IoTOo8vCayWFlvMnctus2VzbCCnNtFnlM +R58qIbw+u1uESiQx2+YB8de/O9DhWLXjxqURV4Qb4OBhVuxVnACJkXnOdV70l+yN +Am6R5p1mDkPqzkklA2Nl1xcjd1IFMqkgFBmP/JeLLI/3oH3nzVu6WvA8oxHuYF4C +odojpPrnISwYYVCw0MkcYbGICbAPJHD2TZKFQ5QHolJgs+rn7c2e6vZj6nAspZU8 +4ebHXWf8jX7+T2eI2S7T8LmQV2lhIwJeKhvevrBQQtecqdQn+nFPRjsrwmeZ48Zw +S3UL33dRhAmImny3vzgPDYCLCF0WiylmJfUT+sDTxfa1KW473f7S3KKLtx184vFO +zjAUwHuSAmeO17I8Eyzdv0RcFx6YWKaVcGNvYzuFHtGQ3R0dYN0fPZ01sy53Nh42 +amfW9wQBD0MstgsxDWDfS4N6zwKSHAduAYsRHsSOwa06A+5hVdLworU= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFLTCCAxWgAwIBAgIUARYnemLQ0i/Ml55sf+x1BmUme1gwDQYJKoZIhvcNAQEN +BQAwLTETMBEGA1UECgwKaW8uc3RyaW16aTEWMBQGA1UEAwwNY2x1c3Rlci1jYSB2 +MDAeFw0yNjAxMTYxNDEzMzhaFw0yNzAxMTYxNDEzMzhaMC0xEzARBgNVBAoMCmlv +LnN0cmltemkxFjAUBgNVBAMMDWNsdXN0ZXItY2EgdjAwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCzmMknBFpQuFkvgyxKOq97A0/elsyiplC6BSi818Z/ +0AKhYKf91EDUyfG2HhUsjrnpE3yv1B3DhE8mae4JKKeN+5M7/NJsJe5nfrkxQ2pL +AQtOpxiD0J4EAl5TINQS8sU4dnllHCbiegepPO22+jsoLqt9l4wJmETpgmaVftCC +/XbKrXng8wGr8xFYIUsJ/JgB1mn+w7oIgib3RQsk3nclHkn7IUs0mXxznzdj49nU +nMCgtEg9CA9K8r/WQjij1AY3auaGAQN4i1uvzk2/mOjcTRjXdWe30asRlfvPoxZn +ouIAJSokync5dP630jLWEXvGZSmqiXwAffCOpfdPyTazWhT+IQ/p/mk6dFZnQhIT +tLEVJCHji3Y7JmuKEXgfKHuqI4CNX6VD/NSlTFSrShodNqbtKJp0ecIg+jX8tKvd +u9dNgziCcCJAKOfTNO2gs3EZ2TPH6kxxOCeLCJ60QFifrN0trs63HRuY86CS3XNd +10jpDHpOVSCWa09+g8Zk/jf8VyybzUtu1APxJMlhze8BrOx5mIQWBQds7EqCa7vs +0PaptOYHJ8JMl69pLFzYDWRWdNVSc41GHMeLX5Hg9ENRjGq4eXJQ18SKw6vZMEtV +PTG+mKGibA0kgKyf1ftSjC8urkfqYRVViHw/MPtjAWwer2QXXWHkjXklfh1OVhFZ +BwIDAQABo0UwQzAdBgNVHQ4EFgQU0HiVPUTU568VwcYvEWiN5EOmN0QwEgYDVR0T +AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQADggIB +ACkXOjCMFtEZL2BSMYi/rWbHAht2K67yj8kiT3Xru+tZ0grS6KNxb3xtpXqyxH/z +j1jDW7vfW0vopTZRd0fZP7re+qHwVcEzaQfLisJ8IVmmMKOlhDXC0uD8zWH56g0/ +7gw9hDjJeNNsZLxkNd4e+rv9wJxVPm0DZ6h17rv3gMz5c24pqsv/5qO+HofE+Y46 +2jMS5t+ZzmpbYVoY0rzvkkACGjwcQ/Cr5WPEDIh1ndLXn7iIPnhNUwwLM1+97y00 +tGrEIXuL5r1p2ZZYT6KX/1yOVV6jPZXxbtD2mGWsVcmrWoaJfwkrAqaRjonPSQGB +sEnLDYdyl0hkZivd7ti+UIDxy9hAD9DA2lM1+0MTG8a2u6slAZDBvkbLOeywK+g4 +WNcGLz0zxulN1x3rGL5WOPaf7lA3hSO9p9kDT7RXkawmw/mQLCl5tX4qSpyVvIZz +6kdnxss+vsgb6qjqc8QMV+P6cXBWEaxcd2KexSmBBCg6FrjomFBMusw5tjcvdqwi +jAGY1S8vodQDAbDMmidOgYdaR+hNdPPM+U1wlR0wPJx0BiVzJA+GUaVqsu+hCcw/ +c75SgDNerU7oO9bPxNeE+0lyUm813IhhAJWjkMgSGWyys53Gs6y28cBEFlA962Rs +vSSSsYPkKrwL+sDvG8Ut+xQ4cshAFCyShOfV3MRwoEDL +-----END CERTIFICATE----- diff --git a/smart-log-analyzer/correlator/kaoto-datamapper-4a94acc3.xsl b/smart-log-analyzer/correlator/kaoto-datamapper-4a94acc3.xsl new file mode 100644 index 0000000..1e9e7f4 --- /dev/null +++ b/smart-log-analyzer/correlator/kaoto-datamapper-4a94acc3.xsl @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- This file is generated by Kaoto DataMapper. Do not edit. --> +<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions"> + <xsl:output method="text" indent="yes"/> + <xsl:param name="logs"/> + <xsl:variable name="logs-x" select="json-to-xml($logs)"/> + <xsl:variable name="mapped-xml"> + <map xmlns="http://www.w3.org/2005/xpath-functions"> + <string key="timeUnixNano"> + <xsl:value-of select="$logs-x/fn:map/fn:string[@key='timeUnixNano']"/> + </string> + <string key="observedTimeUnixNano"> + <xsl:value-of select="$logs-x/fn:map/fn:string[@key='observedTimeUnixNano']"/> + </string> + <string key="severityText"> + <xsl:value-of select="$logs-x/fn:map/fn:string[@key='severityText']"/> + </string> + <string key="message"> + <xsl:value-of select="$logs-x/fn:map/fn:map[@key='body']/fn:string[@key='stringValue']"/> + <xsl:for-each select="$logs-x/fn:map/fn:array[@key='attributes']/fn:map"> + <xsl:value-of select="concat(' ', fn:string[@key='key'], ': ', fn:map[@key='value']/fn:string[@key='stringValue'])"/> + </xsl:for-each> + </string> + <string key="traceId"> + <xsl:value-of select="$logs-x/fn:map/fn:string[@key='traceId']"/> + </string> + <string key="spanId"> + <xsl:value-of select="$logs-x/fn:map/fn:string[@key='spanId']"/> + </string> + <string key="type"> + <xsl:value-of select="'log'"/> + </string> + </map> + </xsl:variable> + <xsl:template match="/"> + <xsl:value-of select="xml-to-json($mapped-xml)"/> + </xsl:template> +</xsl:stylesheet> \ No newline at end of file diff --git a/smart-log-analyzer/correlator/kaoto-datamapper-8f5bb2dd.xsl b/smart-log-analyzer/correlator/kaoto-datamapper-8f5bb2dd.xsl new file mode 100644 index 0000000..c818b32 --- /dev/null +++ b/smart-log-analyzer/correlator/kaoto-datamapper-8f5bb2dd.xsl @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- This file is generated by Kaoto DataMapper. Do not edit. --> +<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions"> + <xsl:output method="text" indent="yes"/> + <xsl:param name="traces"/> + <xsl:variable name="traces-x" select="json-to-xml($traces)"/> + <xsl:variable name="mapped-xml"> + <map xmlns="http://www.w3.org/2005/xpath-functions"> + <string key="type"> + <xsl:value-of select="'trace'"/> + </string> + <string key="timeUnixNano"> + <xsl:value-of select="$traces-x/fn:map/fn:string[@key='startTimeUnixNano']"/> + </string> + <string key="durationNano"> + <xsl:value-of select="xs:integer($traces-x/fn:map/fn:string[@key='endTimeUnixNano']) - xs:integer($traces-x/fn:map/fn:string[@key='startTimeUnixNano'])"/> + </string> + <string key="traceId"> + <xsl:value-of select="$traces-x/fn:map/fn:string[@key='traceId']"/> + </string> + <string key="spanId"> + <xsl:value-of select="$traces-x/fn:map/fn:string[@key='spanId']"/> + </string> + <xsl:if test="$traces-x/fn:map/fn:string[@key='parentSpanId']"> + <string key="parentSpanId"> + <xsl:value-of select="$traces-x/fn:map/fn:string[@key='parentSpanId']"/> + </string> + </xsl:if> + <string key="name"> + <xsl:value-of select="$traces-x/fn:map/fn:string[@key='name']"/> + </string> + <string key="kind"> + <xsl:value-of select="$traces-x/fn:map/fn:number[@key='kind']"/> + </string> + <string key="status"> + <xsl:value-of select="$traces-x/fn:map/fn:map[@key='status']/fn:number[@key='code']"/> + </string> + <string key="message"> + <xsl:for-each select="$traces-x/fn:map/fn:array[@key='attributes']/fn:map"> + <xsl:choose> + <xsl:when test="fn:map[@key='value']/fn:string[@key='stringValue']"> + <xsl:value-of select="concat(' ', fn:string[@key='key'], ': ', fn:map[@key='value']/fn:string[@key='stringValue'])"/> + </xsl:when> + <xsl:when test="fn:map[@key='value']/fn:string[@key='intValue']"> + <xsl:value-of select="concat(' ', fn:string[@key='key'], ': ', fn:map[@key='value']/fn:string[@key='intValue'])"/> + </xsl:when> + <xsl:when test="fn:map[@key='value']/fn:number[@key='intValue']"> + <xsl:value-of select="concat(' ', fn:string[@key='key'], ': ', fn:map[@key='value']/fn:number[@key='intValue'])"/> + </xsl:when> + </xsl:choose> + </xsl:for-each> + <xsl:for-each select="$traces-x/fn:map/fn:array[@key='events']/fn:map"> + <xsl:value-of select="concat(' [Event] ', fn:string[@key='name'])"/> + <xsl:for-each select="fn:array[@key='attributes']/fn:map"> + <xsl:value-of select="concat(' ', fn:string[@key='key'], ': ', fn:map[@key='value']/fn:string[@key='stringValue'])"/> + </xsl:for-each> + </xsl:for-each> + </string> + </map> + </xsl:variable> + <xsl:template match="/"> + <xsl:value-of select="xml-to-json($mapped-xml)"/> + </xsl:template> +</xsl:stylesheet> \ No newline at end of file diff --git a/smart-log-analyzer/correlator/logs-mapper.camel.yaml b/smart-log-analyzer/correlator/logs-mapper.camel.yaml new file mode 100644 index 0000000..f4f85df --- /dev/null +++ b/smart-log-analyzer/correlator/logs-mapper.camel.yaml @@ -0,0 +1,45 @@ +- route: + id: log-consumer + from: + uri: kafka:{{camel.kafka.topic.logs}} + parameters: + autoOffsetReset: earliest + groupId: correlator + steps: + - log: + loggingLevel: DEBUG + message: Received log from Kafka \n ${body} + - split: + id: split-logs + expression: + jsonpath: + expression: $.resourceLogs[*].scopeLogs[*].logRecords[*] + writeAsString: true + steps: + - setHeader: + id: set-logs-header + expression: + simple: + expression: ${body} + name: logs + - setBody: + id: set-dummy-xml + expression: + constant: + expression: <dummy/> + - step: + id: kaoto-datamapper-4a94acc3 + steps: + - to: + id: kaoto-datamapper-xslt + uri: xslt-saxon:kaoto-datamapper-4a94acc3.xsl + parameters: + allowStAX: true + - log: + loggingLevel: DEBUG + message: "Mapped output: ${body}" + - to: + id: to-2266 + uri: direct + parameters: + name: store diff --git a/smart-log-analyzer/correlator/otel-log-record-schema.json b/smart-log-analyzer/correlator/otel-log-record-schema.json new file mode 100644 index 0000000..afc34fa --- /dev/null +++ b/smart-log-analyzer/correlator/otel-log-record-schema.json @@ -0,0 +1,137 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://opentelemetry.io/schemas/logs/1.24.0/LogRecord", + "title": "OpenTelemetry Log Record", + "description": "JSON Schema for a single OpenTelemetry log record (after split from OTLP batch)", + "type": "object", + "properties": { + "timeUnixNano": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Time when the event occurred (nanoseconds since Unix epoch)" + }, + "observedTimeUnixNano": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Time when the event was observed (nanoseconds since Unix epoch)" + }, + "severityNumber": { + "type": "integer", + "minimum": 0, + "maximum": 24, + "description": "Numerical severity value (1-24)" + }, + "severityText": { + "type": "string", + "description": "Severity text (e.g., TRACE, DEBUG, INFO, WARN, ERROR, FATAL)" + }, + "body": { + "$ref": "#/$defs/AnyValue", + "description": "The body of the log record" + }, + "attributes": { + "type": "array", + "description": "Additional attributes for the log record", + "items": { + "$ref": "#/$defs/KeyValue" + } + }, + "droppedAttributesCount": { + "type": "integer", + "description": "Number of attributes that were dropped due to limits" + }, + "flags": { + "type": "integer", + "description": "Flags for the log record" + }, + "traceId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{32}$", + "description": "Trace ID (32 hex characters)" + }, + "spanId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{16}$", + "description": "Span ID (16 hex characters)" + } + }, + "$defs": { + "KeyValue": { + "type": "object", + "description": "A key-value pair", + "required": ["key", "value"], + "properties": { + "key": { + "type": "string", + "description": "The attribute key" + }, + "value": { + "$ref": "#/$defs/AnyValue", + "description": "The attribute value" + } + } + }, + "AnyValue": { + "type": "object", + "description": "A value of any type", + "properties": { + "stringValue": { + "type": "string" + }, + "boolValue": { + "type": "boolean" + }, + "intValue": { + "type": "string", + "pattern": "^-?[0-9]+$" + }, + "doubleValue": { + "type": "number" + }, + "arrayValue": { + "$ref": "#/$defs/ArrayValue" + }, + "kvlistValue": { + "$ref": "#/$defs/KeyValueList" + }, + "bytesValue": { + "type": "string", + "contentEncoding": "base64" + } + }, + "oneOf": [ + { "required": ["stringValue"] }, + { "required": ["boolValue"] }, + { "required": ["intValue"] }, + { "required": ["doubleValue"] }, + { "required": ["arrayValue"] }, + { "required": ["kvlistValue"] }, + { "required": ["bytesValue"] } + ] + }, + "ArrayValue": { + "type": "object", + "description": "An array of values", + "properties": { + "values": { + "type": "array", + "items": { + "$ref": "#/$defs/AnyValue" + } + } + } + }, + "KeyValueList": { + "type": "object", + "description": "A list of key-value pairs", + "properties": { + "values": { + "type": "array", + "items": { + "$ref": "#/$defs/KeyValue" + } + } + } + } + } +} diff --git a/smart-log-analyzer/correlator/otel-logs-schema.json b/smart-log-analyzer/correlator/otel-logs-schema.json new file mode 100644 index 0000000..ffa7674 --- /dev/null +++ b/smart-log-analyzer/correlator/otel-logs-schema.json @@ -0,0 +1,227 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://opentelemetry.io/schemas/logs/1.24.0", + "title": "OpenTelemetry Log Data", + "description": "JSON Schema for OpenTelemetry Protocol (OTLP) log data", + "type": "object", + "required": ["resourceLogs"], + "properties": { + "resourceLogs": { + "type": "array", + "description": "Array of ResourceLogs messages", + "items": { + "$ref": "#/$defs/ResourceLogs" + } + } + }, + "$defs": { + "ResourceLogs": { + "type": "object", + "description": "A collection of ScopeLogs from a Resource", + "properties": { + "resource": { + "$ref": "#/$defs/Resource" + }, + "scopeLogs": { + "type": "array", + "description": "A list of ScopeLogs that originate from a resource", + "items": { + "$ref": "#/$defs/ScopeLogs" + } + }, + "schemaUrl": { + "type": "string", + "format": "uri", + "description": "Schema URL for the resource" + } + } + }, + "Resource": { + "type": "object", + "description": "Resource information", + "properties": { + "attributes": { + "type": "array", + "description": "Set of attributes that describe the resource", + "items": { + "$ref": "#/$defs/KeyValue" + } + } + } + }, + "ScopeLogs": { + "type": "object", + "description": "A collection of log records from an instrumentation scope", + "properties": { + "scope": { + "$ref": "#/$defs/InstrumentationScope" + }, + "logRecords": { + "type": "array", + "description": "A list of log records", + "items": { + "$ref": "#/$defs/LogRecord" + } + }, + "schemaUrl": { + "type": "string", + "format": "uri", + "description": "Schema URL for the scope" + } + } + }, + "InstrumentationScope": { + "type": "object", + "description": "Instrumentation scope information", + "properties": { + "name": { + "type": "string", + "description": "Name of the instrumentation scope" + }, + "version": { + "type": "string", + "description": "Version of the instrumentation scope" + }, + "attributes": { + "type": "array", + "description": "Attributes of the instrumentation scope", + "items": { + "$ref": "#/$defs/KeyValue" + } + } + } + }, + "LogRecord": { + "type": "object", + "description": "A log record", + "properties": { + "timeUnixNano": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Time when the event occurred (nanoseconds since Unix epoch)" + }, + "observedTimeUnixNano": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Time when the event was observed (nanoseconds since Unix epoch)" + }, + "severityNumber": { + "type": "integer", + "minimum": 0, + "maximum": 24, + "description": "Numerical severity value (1-24)" + }, + "severityText": { + "type": "string", + "description": "Severity text (e.g., TRACE, DEBUG, INFO, WARN, ERROR, FATAL)" + }, + "body": { + "$ref": "#/$defs/AnyValue", + "description": "The body of the log record" + }, + "attributes": { + "type": "array", + "description": "Additional attributes for the log record", + "items": { + "$ref": "#/$defs/KeyValue" + } + }, + "droppedAttributesCount": { + "type": "integer", + "description": "Number of attributes that were dropped due to limits" + }, + "flags": { + "type": "integer", + "description": "Flags for the log record" + }, + "traceId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{32}$", + "description": "Trace ID (32 hex characters)" + }, + "spanId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{16}$", + "description": "Span ID (16 hex characters)" + } + } + }, + "KeyValue": { + "type": "object", + "description": "A key-value pair", + "required": ["key", "value"], + "properties": { + "key": { + "type": "string", + "description": "The attribute key" + }, + "value": { + "$ref": "#/$defs/AnyValue", + "description": "The attribute value" + } + } + }, + "AnyValue": { + "type": "object", + "description": "A value of any type", + "properties": { + "stringValue": { + "type": "string" + }, + "boolValue": { + "type": "boolean" + }, + "intValue": { + "type": "string", + "pattern": "^-?[0-9]+$" + }, + "doubleValue": { + "type": "number" + }, + "arrayValue": { + "$ref": "#/$defs/ArrayValue" + }, + "kvlistValue": { + "$ref": "#/$defs/KeyValueList" + }, + "bytesValue": { + "type": "string", + "contentEncoding": "base64" + } + }, + "oneOf": [ + { "required": ["stringValue"] }, + { "required": ["boolValue"] }, + { "required": ["intValue"] }, + { "required": ["doubleValue"] }, + { "required": ["arrayValue"] }, + { "required": ["kvlistValue"] }, + { "required": ["bytesValue"] } + ] + }, + "ArrayValue": { + "type": "object", + "description": "An array of values", + "properties": { + "values": { + "type": "array", + "items": { + "$ref": "#/$defs/AnyValue" + } + } + } + }, + "KeyValueList": { + "type": "object", + "description": "A list of key-value pairs", + "properties": { + "values": { + "type": "array", + "items": { + "$ref": "#/$defs/KeyValue" + } + } + } + } + } +} diff --git a/smart-log-analyzer/correlator/otel-span-schema.json b/smart-log-analyzer/correlator/otel-span-schema.json new file mode 100644 index 0000000..9b12c69 --- /dev/null +++ b/smart-log-analyzer/correlator/otel-span-schema.json @@ -0,0 +1,244 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://opentelemetry.io/schemas/traces/1.24.0/Span", + "title": "OpenTelemetry Span", + "description": "JSON Schema for a single OpenTelemetry span (after split from OTLP batch)", + "type": "object", + "properties": { + "traceId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{32}$", + "description": "Trace ID (32 hex characters)" + }, + "spanId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{16}$", + "description": "Span ID (16 hex characters)" + }, + "traceState": { + "type": "string", + "description": "W3C trace state" + }, + "parentSpanId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{16}$", + "description": "Parent span ID (16 hex characters)" + }, + "flags": { + "type": "integer", + "description": "Trace flags (W3C format)" + }, + "name": { + "type": "string", + "description": "Name of the span" + }, + "kind": { + "type": "integer", + "minimum": 0, + "maximum": 5, + "description": "Span kind (0=UNSPECIFIED, 1=INTERNAL, 2=SERVER, 3=CLIENT, 4=PRODUCER, 5=CONSUMER)" + }, + "startTimeUnixNano": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Start time (nanoseconds since Unix epoch)" + }, + "endTimeUnixNano": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "End time (nanoseconds since Unix epoch)" + }, + "attributes": { + "type": "array", + "description": "Attributes for the span", + "items": { + "$ref": "#/$defs/KeyValue" + } + }, + "droppedAttributesCount": { + "type": "integer", + "description": "Number of attributes that were dropped due to limits" + }, + "events": { + "type": "array", + "description": "Timed events within the span", + "items": { + "$ref": "#/$defs/SpanEvent" + } + }, + "droppedEventsCount": { + "type": "integer", + "description": "Number of events that were dropped due to limits" + }, + "links": { + "type": "array", + "description": "Links to other spans", + "items": { + "$ref": "#/$defs/SpanLink" + } + }, + "droppedLinksCount": { + "type": "integer", + "description": "Number of links that were dropped due to limits" + }, + "status": { + "$ref": "#/$defs/Status" + } + }, + "$defs": { + "SpanEvent": { + "type": "object", + "description": "A timed event within a span", + "properties": { + "timeUnixNano": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Time of the event (nanoseconds since Unix epoch)" + }, + "name": { + "type": "string", + "description": "Name of the event" + }, + "attributes": { + "type": "array", + "description": "Attributes for the event", + "items": { + "$ref": "#/$defs/KeyValue" + } + }, + "droppedAttributesCount": { + "type": "integer", + "description": "Number of attributes that were dropped due to limits" + } + } + }, + "SpanLink": { + "type": "object", + "description": "A link to another span", + "properties": { + "traceId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{32}$", + "description": "Trace ID of the linked span" + }, + "spanId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{16}$", + "description": "Span ID of the linked span" + }, + "traceState": { + "type": "string", + "description": "W3C trace state" + }, + "attributes": { + "type": "array", + "description": "Attributes for the link", + "items": { + "$ref": "#/$defs/KeyValue" + } + }, + "droppedAttributesCount": { + "type": "integer", + "description": "Number of attributes that were dropped due to limits" + }, + "flags": { + "type": "integer", + "description": "Trace flags" + } + } + }, + "Status": { + "type": "object", + "description": "Status of the span", + "properties": { + "message": { + "type": "string", + "description": "Human-readable status message" + }, + "code": { + "type": "integer", + "minimum": 0, + "maximum": 2, + "description": "Status code (0=UNSET, 1=OK, 2=ERROR)" + } + } + }, + "KeyValue": { + "type": "object", + "description": "A key-value pair", + "required": ["key", "value"], + "properties": { + "key": { + "type": "string", + "description": "The attribute key" + }, + "value": { + "$ref": "#/$defs/AnyValue", + "description": "The attribute value" + } + } + }, + "AnyValue": { + "type": "object", + "description": "A value of any type", + "properties": { + "stringValue": { + "type": "string" + }, + "boolValue": { + "type": "boolean" + }, + "intValue": { + "type": "string", + "pattern": "^-?[0-9]+$" + }, + "doubleValue": { + "type": "number" + }, + "arrayValue": { + "$ref": "#/$defs/ArrayValue" + }, + "kvlistValue": { + "$ref": "#/$defs/KeyValueList" + }, + "bytesValue": { + "type": "string", + "contentEncoding": "base64" + } + }, + "oneOf": [ + { "required": ["stringValue"] }, + { "required": ["boolValue"] }, + { "required": ["intValue"] }, + { "required": ["doubleValue"] }, + { "required": ["arrayValue"] }, + { "required": ["kvlistValue"] }, + { "required": ["bytesValue"] } + ] + }, + "ArrayValue": { + "type": "object", + "description": "An array of values", + "properties": { + "values": { + "type": "array", + "items": { + "$ref": "#/$defs/AnyValue" + } + } + } + }, + "KeyValueList": { + "type": "object", + "description": "A list of key-value pairs", + "properties": { + "values": { + "type": "array", + "items": { + "$ref": "#/$defs/KeyValue" + } + } + } + } + } +} diff --git a/smart-log-analyzer/correlator/otel-traces-schema.json b/smart-log-analyzer/correlator/otel-traces-schema.json new file mode 100644 index 0000000..6d05e12 --- /dev/null +++ b/smart-log-analyzer/correlator/otel-traces-schema.json @@ -0,0 +1,334 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://opentelemetry.io/schemas/traces/1.24.0", + "title": "OpenTelemetry Trace Data", + "description": "JSON Schema for OpenTelemetry Protocol (OTLP) trace data", + "type": "object", + "required": ["resourceSpans"], + "properties": { + "resourceSpans": { + "type": "array", + "description": "Array of ResourceSpans messages", + "items": { + "$ref": "#/$defs/ResourceSpans" + } + } + }, + "$defs": { + "ResourceSpans": { + "type": "object", + "description": "A collection of ScopeSpans from a Resource", + "properties": { + "resource": { + "$ref": "#/$defs/Resource" + }, + "scopeSpans": { + "type": "array", + "description": "A list of ScopeSpans that originate from a resource", + "items": { + "$ref": "#/$defs/ScopeSpans" + } + }, + "schemaUrl": { + "type": "string", + "format": "uri", + "description": "Schema URL for the resource" + } + } + }, + "Resource": { + "type": "object", + "description": "Resource information", + "properties": { + "attributes": { + "type": "array", + "description": "Set of attributes that describe the resource", + "items": { + "$ref": "#/$defs/KeyValue" + } + } + } + }, + "ScopeSpans": { + "type": "object", + "description": "A collection of spans from an instrumentation scope", + "properties": { + "scope": { + "$ref": "#/$defs/InstrumentationScope" + }, + "spans": { + "type": "array", + "description": "A list of spans", + "items": { + "$ref": "#/$defs/Span" + } + }, + "schemaUrl": { + "type": "string", + "format": "uri", + "description": "Schema URL for the scope" + } + } + }, + "InstrumentationScope": { + "type": "object", + "description": "Instrumentation scope information", + "properties": { + "name": { + "type": "string", + "description": "Name of the instrumentation scope" + }, + "version": { + "type": "string", + "description": "Version of the instrumentation scope" + }, + "attributes": { + "type": "array", + "description": "Attributes of the instrumentation scope", + "items": { + "$ref": "#/$defs/KeyValue" + } + } + } + }, + "Span": { + "type": "object", + "description": "A span representing a trace segment", + "properties": { + "traceId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{32}$", + "description": "Trace ID (32 hex characters)" + }, + "spanId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{16}$", + "description": "Span ID (16 hex characters)" + }, + "traceState": { + "type": "string", + "description": "W3C trace state" + }, + "parentSpanId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{16}$", + "description": "Parent span ID (16 hex characters)" + }, + "flags": { + "type": "integer", + "description": "Trace flags (W3C format)" + }, + "name": { + "type": "string", + "description": "Name of the span" + }, + "kind": { + "type": "integer", + "minimum": 0, + "maximum": 5, + "description": "Span kind (0=UNSPECIFIED, 1=INTERNAL, 2=SERVER, 3=CLIENT, 4=PRODUCER, 5=CONSUMER)" + }, + "startTimeUnixNano": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Start time (nanoseconds since Unix epoch)" + }, + "endTimeUnixNano": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "End time (nanoseconds since Unix epoch)" + }, + "attributes": { + "type": "array", + "description": "Attributes for the span", + "items": { + "$ref": "#/$defs/KeyValue" + } + }, + "droppedAttributesCount": { + "type": "integer", + "description": "Number of attributes that were dropped due to limits" + }, + "events": { + "type": "array", + "description": "Timed events within the span", + "items": { + "$ref": "#/$defs/SpanEvent" + } + }, + "droppedEventsCount": { + "type": "integer", + "description": "Number of events that were dropped due to limits" + }, + "links": { + "type": "array", + "description": "Links to other spans", + "items": { + "$ref": "#/$defs/SpanLink" + } + }, + "droppedLinksCount": { + "type": "integer", + "description": "Number of links that were dropped due to limits" + }, + "status": { + "$ref": "#/$defs/Status" + } + } + }, + "SpanEvent": { + "type": "object", + "description": "A timed event within a span", + "properties": { + "timeUnixNano": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Time of the event (nanoseconds since Unix epoch)" + }, + "name": { + "type": "string", + "description": "Name of the event" + }, + "attributes": { + "type": "array", + "description": "Attributes for the event", + "items": { + "$ref": "#/$defs/KeyValue" + } + }, + "droppedAttributesCount": { + "type": "integer", + "description": "Number of attributes that were dropped due to limits" + } + } + }, + "SpanLink": { + "type": "object", + "description": "A link to another span", + "properties": { + "traceId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{32}$", + "description": "Trace ID of the linked span" + }, + "spanId": { + "type": "string", + "pattern": "^[a-fA-F0-9]{16}$", + "description": "Span ID of the linked span" + }, + "traceState": { + "type": "string", + "description": "W3C trace state" + }, + "attributes": { + "type": "array", + "description": "Attributes for the link", + "items": { + "$ref": "#/$defs/KeyValue" + } + }, + "droppedAttributesCount": { + "type": "integer", + "description": "Number of attributes that were dropped due to limits" + }, + "flags": { + "type": "integer", + "description": "Trace flags" + } + } + }, + "Status": { + "type": "object", + "description": "Status of the span", + "properties": { + "message": { + "type": "string", + "description": "Human-readable status message" + }, + "code": { + "type": "integer", + "minimum": 0, + "maximum": 2, + "description": "Status code (0=UNSET, 1=OK, 2=ERROR)" + } + } + }, + "KeyValue": { + "type": "object", + "description": "A key-value pair", + "required": ["key", "value"], + "properties": { + "key": { + "type": "string", + "description": "The attribute key" + }, + "value": { + "$ref": "#/$defs/AnyValue", + "description": "The attribute value" + } + } + }, + "AnyValue": { + "type": "object", + "description": "A value of any type", + "properties": { + "stringValue": { + "type": "string" + }, + "boolValue": { + "type": "boolean" + }, + "intValue": { + "type": "string", + "pattern": "^-?[0-9]+$" + }, + "doubleValue": { + "type": "number" + }, + "arrayValue": { + "$ref": "#/$defs/ArrayValue" + }, + "kvlistValue": { + "$ref": "#/$defs/KeyValueList" + }, + "bytesValue": { + "type": "string", + "contentEncoding": "base64" + } + }, + "oneOf": [ + { "required": ["stringValue"] }, + { "required": ["boolValue"] }, + { "required": ["intValue"] }, + { "required": ["doubleValue"] }, + { "required": ["arrayValue"] }, + { "required": ["kvlistValue"] }, + { "required": ["bytesValue"] } + ] + }, + "ArrayValue": { + "type": "object", + "description": "An array of values", + "properties": { + "values": { + "type": "array", + "items": { + "$ref": "#/$defs/AnyValue" + } + } + } + }, + "KeyValueList": { + "type": "object", + "description": "A list of key-value pairs", + "properties": { + "values": { + "type": "array", + "items": { + "$ref": "#/$defs/KeyValue" + } + } + } + } + } +} diff --git a/smart-log-analyzer/correlator/traces-mapper.camel.yaml b/smart-log-analyzer/correlator/traces-mapper.camel.yaml new file mode 100644 index 0000000..c75721a --- /dev/null +++ b/smart-log-analyzer/correlator/traces-mapper.camel.yaml @@ -0,0 +1,45 @@ +- route: + id: trace-consumer + from: + uri: kafka:{{camel.kafka.topic.spans}} + parameters: + autoOffsetReset: earliest + groupId: correlator + steps: + - log: + loggingLevel: DEBUG + message: Received trace from Kafka + - split: + id: split-spans + expression: + jsonpath: + expression: $.resourceSpans[*].scopeSpans[*].spans[*] + writeAsString: true + steps: + - setHeader: + id: set-traces-header + expression: + simple: + expression: ${body} + name: traces + - setBody: + id: set-dummy-xml-1 + expression: + constant: + expression: <dummy/> + - step: + id: kaoto-datamapper-8f5bb2dd + steps: + - to: + id: kaoto-datamapper-xslt-1526 + uri: xslt-saxon:kaoto-datamapper-8f5bb2dd.xsl + parameters: + allowStAX: true + - log: + loggingLevel: DEBUG + message: "Mapped output: ${body}" + - to: + id: to-22661 + uri: direct + parameters: + name: store diff --git a/smart-log-analyzer/first-iteration/analyzer.camel.yaml b/smart-log-analyzer/first-iteration/analyzer.camel.yaml new file mode 100644 index 0000000..01cc80d --- /dev/null +++ b/smart-log-analyzer/first-iteration/analyzer.camel.yaml @@ -0,0 +1,62 @@ +# Consumes log events from Kafka, aggregates by traceId, and sends +# to LLM for analysis when an ERROR level event is detected +- route: + id: log-event-consumer + from: + uri: kafka:log-events + parameters: + autoOffsetReset: earliest + brokers: localhost:9092 + groupId: log-analyzer + steps: + - log: + loggingLevel: DEBUG + message: "Received event from Kafka: ${body}" + - setVariable: + name: traceId + jq: + expression: .traceId + resultType: java.lang.String + - setVariable: + name: level + jq: + expression: .level + resultType: java.lang.String + - aggregate: + correlationExpression: + simple: + expression: ${variable.traceId} + aggregationStrategy: "#stringAggregationStrategy" + completionPredicate: + simple: + expression: ${variable.level} == 'ERROR' + completionTimeout: 30000 + steps: + - log: + message: "Aggregation completed for traceId: ${variable.traceId} - sending to LLM" + - setVariable: + name: eventsJson + simple: ${body} + - setBody: + simple: > + Analyze the following log events for traceId ${variable.traceId}. + + An ERROR was detected. Identify the root cause, explain what + happened in the sequence of events, and suggest possible fixes. + + Events: ${variable.eventsJson} + - to: + uri: openai:chat-completion + parameters: + apiKey: not-needed + baseUrl: http://localhost:11434/v1 + model: granite4:3b + systemMessage: > + Analyze log events to identify errors, their root causes, and + provide actionable recommendations. Be concise and focus + on the error chain and sequence of events. + - log: + message: "LLM Analysis for traceId ${variable.traceId}: ${body}" +- beans: + - name: stringAggregationStrategy + type: org.apache.camel.processor.aggregate.StringAggregationStrategy \ No newline at end of file diff --git a/smart-log-analyzer/first-iteration/application.properties b/smart-log-analyzer/first-iteration/application.properties new file mode 100644 index 0000000..caa88c5 --- /dev/null +++ b/smart-log-analyzer/first-iteration/application.properties @@ -0,0 +1,2 @@ +# Dependencies for the first-iteration analyzer +camel.jbang.dependencies=org.apache.camel:camel-openai diff --git a/smart-log-analyzer/first-iteration/load-generator.camel.yaml b/smart-log-analyzer/first-iteration/load-generator.camel.yaml new file mode 100644 index 0000000..3a8f248 --- /dev/null +++ b/smart-log-analyzer/first-iteration/load-generator.camel.yaml @@ -0,0 +1,48 @@ +- route: + id: event-generator + from: + uri: timer:eventGenerator + parameters: + period: 5000 + steps: + # Generate a new traceId for each timer event + - setVariable: + name: traceId + simple: "${random(10000,99999)}-${date:now:yyyyMMddHHmmssSSS}" + + # Send first INFO event + - setBody: + simple: | + {"traceId": "${variable.traceId}", "level": "INFO", "message": "Application started processing request", "timestamp": "${date:now:yyyy-MM-dd'T'HH:mm:ss.SSSZ}"} + - to: + uri: kafka:log-events + parameters: + brokers: localhost:9092 + + # Send second INFO event + - setBody: + simple: | + {"traceId": "${variable.traceId}", "level": "INFO", "message": "Request validation completed successfully", "timestamp": "${date:now:yyyy-MM-dd'T'HH:mm:ss.SSSZ}"} + - to: + uri: kafka:log-events + parameters: + brokers: localhost:9092 + + # 30% chance of generating an ERROR event + - choice: + when: + - simple: "${random(1,10)} <= 3" + steps: + - setBody: + simple: | + {"traceId": "${variable.traceId}", "level": "ERROR", "message": "Unexpected error occurred during processing", "timestamp": "${date:now:yyyy-MM-dd'T'HH:mm:ss.SSSZ}"} + - to: + uri: kafka:log-events + parameters: + brokers: localhost:9092 + - log: + message: "ERROR event sent for traceId: ${variable.traceId}" + otherwise: + steps: + - log: + message: "No error generated for traceId: ${variable.traceId}" \ No newline at end of file diff --git a/smart-log-analyzer/first-iteration/test/first-iteration.camel.it.yaml b/smart-log-analyzer/first-iteration/test/first-iteration.camel.it.yaml new file mode 100644 index 0000000..900b164 --- /dev/null +++ b/smart-log-analyzer/first-iteration/test/first-iteration.camel.it.yaml @@ -0,0 +1,46 @@ +name: first-iteration-test +description: Integration test for the first-iteration log analyzer prototype +variables: + - name: kafka.broker + value: localhost:9092 + - name: kafka.topic + value: log-events +actions: + - camel: + infra: + run: + service: kafka + - camel: + jbang: + run: + stub: + - "openai:*" + integration: + name: "log-analyzer" + file: "../analyzer.camel.yaml" + systemProperties: + file: "../application.properties" + - send: + endpoint: | + camel:kafka:${kafka.topic}?brokers=${kafka.broker} + message: + body: + resource: + file: "payload-info.json" + - send: + endpoint: | + camel:kafka:${kafka.topic}?brokers=${kafka.broker} + message: + body: + resource: + file: "payload-error.json" + - camel: + jbang: + verify: + integration: "log-analyzer" + logMessage: "Aggregation completed for traceId" + - camel: + jbang: + verify: + integration: "log-analyzer" + logMessage: "LLM Analysis for traceId test-12345" diff --git a/smart-log-analyzer/first-iteration/test/jbang.properties b/smart-log-analyzer/first-iteration/test/jbang.properties new file mode 100644 index 0000000..8be759f --- /dev/null +++ b/smart-log-analyzer/first-iteration/test/jbang.properties @@ -0,0 +1,3 @@ +run.deps=org.citrusframework:citrus-kafka:4.9.1,\ + org.apache.camel:camel-test-infra-kafka:4.17.0,\ + org.apache.camel:camel-kafka:4.17.0 diff --git a/smart-log-analyzer/first-iteration/test/payload-error.json b/smart-log-analyzer/first-iteration/test/payload-error.json new file mode 100644 index 0000000..2a7dd86 --- /dev/null +++ b/smart-log-analyzer/first-iteration/test/payload-error.json @@ -0,0 +1 @@ +{"traceId": "test-12345", "level": "ERROR", "message": "Unexpected error occurred during processing", "timestamp": "2026-01-28T10:00:01.000+0000"} diff --git a/smart-log-analyzer/first-iteration/test/payload-info.json b/smart-log-analyzer/first-iteration/test/payload-info.json new file mode 100644 index 0000000..c2604b1 --- /dev/null +++ b/smart-log-analyzer/first-iteration/test/payload-info.json @@ -0,0 +1 @@ +{"traceId": "test-12345", "level": "INFO", "message": "Application started processing request", "timestamp": "2026-01-28T10:00:00.000+0000"} diff --git a/smart-log-analyzer/log-generator/agent.properties b/smart-log-analyzer/log-generator/agent.properties new file mode 100644 index 0000000..6019ebc --- /dev/null +++ b/smart-log-analyzer/log-generator/agent.properties @@ -0,0 +1,6 @@ +otel.javaagent.debug=false + +otel.service.name=log-generator +otel.traces.exporter=otlp +otel.metrics.exporter=none +otel.logs.exporter=otlp diff --git a/smart-log-analyzer/log-generator/application-dev.properties b/smart-log-analyzer/log-generator/application-dev.properties new file mode 100644 index 0000000..0950ceb --- /dev/null +++ b/smart-log-analyzer/log-generator/application-dev.properties @@ -0,0 +1,3 @@ +camel.opentelemetry2.enabled = true + +camel.jbang.dependencies=org.apache.camel:camel-opentelemetry2 diff --git a/smart-log-analyzer/log-generator/log-generator.camel.yaml b/smart-log-analyzer/log-generator/log-generator.camel.yaml new file mode 100644 index 0000000..dcd316f --- /dev/null +++ b/smart-log-analyzer/log-generator/log-generator.camel.yaml @@ -0,0 +1,87 @@ +# Simulates order processing with random success/failure outcomes (30% failure rate) +# to generate realistic log data for testing the error analyzer +- route: + id: order-processor + from: + uri: timer:orderProcessor + parameters: + period: "{{timer.order.period:2000}}" + fixedRate: true + steps: + + # Generate random order ID + - setVariable: + name: orderId + simple: "ORD-${random(10000,99999)}" + + # Log: Order received + - log: + message: "Received new order ${variable.orderId} from customer ${random(1000,9999)}" + loggingLevel: INFO + logName: com.example.order.OrderService + + # Log: Validation + - log: + message: "Validating order ${variable.orderId}: checking inventory and pricing" + loggingLevel: INFO + logName: com.example.order.ValidationService + + # Simulate random processing outcomes (30% failure rate) + - choice: + when: + - simple: "${random(0,100)} < 30" + steps: + # Simulate different error types + - choice: + when: + - simple: "${random(0,100)} < 33" + steps: + - log: + message: "Connecting to database for order ${variable.orderId}" + loggingLevel: INFO + logName: com.example.order.DatabaseService + - throwException: + message: "Database connection failed: Connection refused to postgres:5432 while processing order ${variable.orderId}" + exceptionType: java.lang.RuntimeException + - simple: "${random(0,100)} < 66" + steps: + - log: + message: "Initiating payment for order ${variable.orderId}" + loggingLevel: INFO + logName: com.example.payment.PaymentGateway + - throwException: + message: "Network timeout while connecting to payment gateway api.stripe.com for order ${variable.orderId}" + exceptionType: java.lang.RuntimeException + otherwise: + steps: + - log: + message: "Verifying user authentication for order ${variable.orderId}" + loggingLevel: INFO + logName: com.example.auth.AuthService + - throwException: + message: "Authentication failed: Invalid JWT token signature for order ${variable.orderId}" + exceptionType: java.lang.RuntimeException + otherwise: + steps: + - log: + message: "Payment processed successfully for order ${variable.orderId}" + loggingLevel: INFO + logName: com.example.payment.PaymentGateway + - log: + message: "Order ${variable.orderId} completed successfully" + loggingLevel: INFO + logName: com.example.order.OrderService + +# Periodic health check logging for system monitoring +- route: + id: health-checker + from: + uri: timer:healthCheck + parameters: + period: 5000 + fixedRate: true + steps: + - log: + message: "Health check: All systems operational" + loggingLevel: DEBUG + logName: com.example.health.HealthChecker diff --git a/smart-log-analyzer/log-generator/opentelemetry-javaagent.jar b/smart-log-analyzer/log-generator/opentelemetry-javaagent.jar new file mode 100644 index 0000000..926195a Binary files /dev/null and b/smart-log-analyzer/log-generator/opentelemetry-javaagent.jar differ diff --git a/smart-log-analyzer/ui-console/application-dev.properties b/smart-log-analyzer/ui-console/application-dev.properties new file mode 100644 index 0000000..7f32b42 --- /dev/null +++ b/smart-log-analyzer/ui-console/application-dev.properties @@ -0,0 +1,31 @@ +# Storage configuration +analyzer.storage.root=/tmp/analyzer-store + +# JMS Queue configuration +camel.jms.queue.analysis-result=analysis-result + +# Artemis JMS configuration +camel.beans.artemisCF = #class:org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory +camel.beans.artemisCF.brokerURL = tcp://localhost:61616 +camel.beans.artemisCF.user = artemis +camel.beans.artemisCF.password = artemis +camel.beans.poolCF = #class:org.messaginghub.pooled.jms.JmsPoolConnectionFactory +camel.beans.poolCF.connectionFactory = #bean:artemisCF +camel.beans.poolCF.maxSessionsPerConnection = 500 +camel.beans.poolCF.connectionIdleTimeout = 20000 +camel.component.jms.connection-factory = #bean:poolCF + +# REST configuration +camel.rest.component=platform-http +camel.rest.binding-mode=off +camel.rest.port=8080 +camel.rest.host=0.0.0.0 + +# Camel configuration +camel.main.name=ui-console + +camel.server.enabled=true +camel.server.staticEnabled=true + +# Dependencies +camel.jbang.dependencies=org.apache.camel:camel-groovy diff --git a/smart-log-analyzer/ui-console/index.html b/smart-log-analyzer/ui-console/index.html new file mode 100644 index 0000000..3474b82 --- /dev/null +++ b/smart-log-analyzer/ui-console/index.html @@ -0,0 +1,287 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Smart Log Analyzer - Console</title> + <link rel="stylesheet" href="https://unpkg.com/[email protected]/css/carbon-components.min.css"> + <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@400;600&display=swap"> + <style> + body { + margin: 0; + padding: 0; + font-family: 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif; + } + + .bx--header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 8000; + } + + .camel-logo { + width: 32px; + height: 32px; + margin-right: 0.75rem; + vertical-align: middle; + } + + .container { + display: flex; + height: 100vh; + padding-top: 48px; + } + + .sidebar { + width: 320px; + background-color: #161616; + color: #f4f4f4; + padding: 1rem; + overflow-y: auto; + border-right: 1px solid #393939; + } + + .main-content { + flex: 1; + padding: 2rem; + overflow-y: auto; + background-color: #f4f4f4; + } + + .trace-list { + list-style: none; + padding: 0; + margin-top: 1rem; + } + + .trace-item { + padding: 0.75rem 1rem; + margin-bottom: 0.5rem; + background-color: #262626; + cursor: pointer; + transition: background-color 0.11s; + border-left: 3px solid transparent; + } + + .trace-item:hover { + background-color: #333333; + } + + .trace-item.active { + background-color: #0f62fe; + border-left-color: #78a9ff; + } + + .trace-id { + font-size: 0.875rem; + word-break: break-all; + font-family: 'IBM Plex Mono', monospace; + } + + .content-body { + background-color: #ffffff; + padding: 1rem; + white-space: pre-wrap; + word-wrap: break-word; + font-family: 'IBM Plex Mono', monospace; + font-size: 0.875rem; + line-height: 1.6; + max-height: calc(100vh - 250px); + overflow-y: auto; + border: 1px solid #e0e0e0; + } + + .empty-state { + text-align: center; + color: #525252; + padding: 4rem 2rem; + } + + .empty-state-icon { + width: 80px; + height: 80px; + margin: 0 auto 1rem; + display: block; + } + + .count-badge { + display: inline-block; + background-color: #0f62fe; + color: white; + padding: 0.25rem 0.75rem; + border-radius: 1rem; + font-size: 0.75rem; + margin-left: 0.5rem; + margin-bottom: 1rem; + } + + .sidebar-header { + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid #393939; + } + + .sidebar-title { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 0.5rem; + } + </style> +</head> +<body class="bx--body"> + <header class="bx--header" role="banner"> + <a class="bx--header__name" href="javascript:void(0)"> + <img src="https://raw.githubusercontent.com/apache/camel/refs/heads/main/docs/img/logo-d.svg" alt="Apache Camel" class="camel-logo"> + <span class="bx--header__name--prefix">Smart Log</span> + Analyzer + </a> + </header> + + <div class="container"> + <div class="sidebar"> + <div class="sidebar-header"> + <div class="sidebar-title">Trace Analysis</div> + <div id="traceCount"></div> + </div> + <div class="bx--form-item"> + <input + type="text" + class="bx--text-input bx--text-input--light" + id="searchBox" + placeholder="Search trace IDs..."> + </div> + <ul class="trace-list" id="traceList"></ul> + </div> + <div class="main-content"> + <div id="contentPanel"></div> + </div> + </div> + + <script> + const API_BASE = '/api/traces'; + let allTraces = []; + let selectedTraceId = null; + + async function loadTraces() { + try { + const response = await fetch(API_BASE); + if (!response.ok) throw new Error('Failed to fetch traces'); + const data = await response.json(); + allTraces = data || []; + renderTraceList(allTraces); + } catch (error) { + console.error('Error loading traces:', error); + document.getElementById('traceList').innerHTML = ` + <div class="bx--inline-notification bx--inline-notification--error" role="alert" style="margin-top: 1rem;"> + <div class="bx--inline-notification__details"> + <div class="bx--inline-notification__text-wrapper"> + <p class="bx--inline-notification__subtitle">Failed to load traces. Make sure the API server is running.</p> + </div> + </div> + </div> + `; + } + } + + function renderTraceList(traces) { + const traceList = document.getElementById('traceList'); + const traceCount = document.getElementById('traceCount'); + + traceCount.innerHTML = `<span class="count-badge">${traces.length} trace${traces.length !== 1 ? 's' : ''}</span>`; + + if (traces.length === 0) { + traceList.innerHTML = '<div style="padding: 1rem; color: #525252; text-align: center;">No traces found</div>'; + return; + } + + traceList.innerHTML = traces.map(trace => ` + <li class="trace-item ${trace.id === selectedTraceId ? 'active' : ''}" + onclick="loadTraceContent('${trace.id}')"> + <div class="trace-id">${escapeHtml(trace.id)}</div> + </li> + `).join(''); + } + + async function loadTraceContent(traceId) { + selectedTraceId = traceId; + renderTraceList(filterTraces(document.getElementById('searchBox').value)); + + const contentPanel = document.getElementById('contentPanel'); + contentPanel.innerHTML = ` + <div class="bx--loading bx--loading--small"> + <svg class="bx--loading__svg" viewBox="0 0 100 100"> + <circle class="bx--loading__stroke" cx="50%" cy="50%" r="44"></circle> + </svg> + </div> + `; + + try { + const response = await fetch(`${API_BASE}/${traceId}`); + if (!response.ok) throw new Error('Trace not found'); + const data = await response.json(); + + contentPanel.innerHTML = ` + <div class="bx--tile" style="padding: 0;"> + <div style="display: flex; justify-content: space-between; align-items: center; padding: 1.5rem; border-bottom: 1px solid #e0e0e0;"> + <h3 class="bx--type-productive-heading-03">Trace: ${escapeHtml(data.id)}</h3> + <button class="bx--btn bx--btn--primary bx--btn--sm" onclick="loadTraceContent('${escapeHtml(traceId)}')"> + Refresh + </button> + </div> + <div style="padding: 1.5rem;"> + <div class="content-body">${escapeHtml(data.content)}</div> + </div> + </div> + `; + } catch (error) { + console.error('Error loading trace content:', error); + contentPanel.innerHTML = ` + <div class="bx--inline-notification bx--inline-notification--error" role="alert"> + <div class="bx--inline-notification__details"> + <div class="bx--inline-notification__text-wrapper"> + <p class="bx--inline-notification__title">Error</p> + <p class="bx--inline-notification__subtitle">Failed to load trace content</p> + </div> + </div> + </div> + `; + } + } + + function filterTraces(searchTerm) { + if (!searchTerm) return allTraces; + const term = searchTerm.toLowerCase(); + return allTraces.filter(trace => + trace.id.toLowerCase().includes(term) + ); + } + + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + document.getElementById('searchBox').addEventListener('input', (e) => { + const filtered = filterTraces(e.target.value); + renderTraceList(filtered); + }); + + // Initial load + document.getElementById('contentPanel').innerHTML = ` + <div class="empty-state"> + <img src="https://raw.githubusercontent.com/apache/camel/refs/heads/main/docs/img/logo-d.svg" alt="Apache Camel" class="empty-state-icon"> + <h3 class="bx--type-productive-heading-03" style="margin-bottom: 0.5rem;">Welcome to Smart Log Analyzer</h3> + <p class="bx--type-body-short-01">Select a trace from the sidebar to view its analysis</p> + </div> + `; + + loadTraces(); + + // Auto-refresh every 15 seconds + setInterval(loadTraces, 15000); + </script> +</body> +</html> diff --git a/smart-log-analyzer/ui-console/jms-file-storage.camel.yaml b/smart-log-analyzer/ui-console/jms-file-storage.camel.yaml new file mode 100644 index 0000000..b426959 --- /dev/null +++ b/smart-log-analyzer/ui-console/jms-file-storage.camel.yaml @@ -0,0 +1,14 @@ +- route: + id: jms-to-file-storage + from: + uri: jms:{{camel.jms.queue.analysis-result}} + steps: + - log: + message: "Received message from JMS queue: ${body}" + - setHeader: + name: CamelFileName + simple: ${header.traceId}.txt + - to: + uri: file:{{analyzer.storage.root}} + - log: + message: "Saved content for traceId: ${header.traceId}" diff --git a/smart-log-analyzer/ui-console/rest-api.camel.yaml b/smart-log-analyzer/ui-console/rest-api.camel.yaml new file mode 100644 index 0000000..e166719 --- /dev/null +++ b/smart-log-analyzer/ui-console/rest-api.camel.yaml @@ -0,0 +1,84 @@ +- rest: + path: /api/traces + get: + - id: list-traces + produces: application/json + to: direct:list-traces + - id: get-trace-by-id + path: /{id} + produces: application/json + to: direct:get-trace + +- route: + id: route-list-traces + from: + uri: direct:list-traces + steps: + - setVariable: + name: storagePath + simple: "{{analyzer.storage.root}}" + - setBody: + groovy: | + import java.nio.file.* + try { + def path = Paths.get(exchange.getVariable('storagePath')) + if (!Files.exists(path)) { + return [] + } + return Files.list(path) + .filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().endsWith('.txt')) + .map(p -> { + def filename = p.getFileName().toString() + def traceId = filename.substring(0, filename.lastIndexOf('.txt')) + return [id: traceId] + }) + .collect() + } catch (Exception e) { + return [] + } + - marshal: + json: + library: Jackson + - setHeader: + name: Content-Type + constant: application/json + +- route: + id: route-get-trace + from: + uri: direct:get-trace + steps: + - setVariable: + name: traceId + simple: ${header.id} + - pollEnrich: + expression: + simple: file:{{analyzer.storage.root}}?fileName=${variable.traceId}.txt&noop=true&idempotent=false + timeout: 1000 + - choice: + when: + - simple: ${body} != null + steps: + - convertBodyTo: + type: String + - setVariable: + name: fileContent + simple: ${body} + - marshal: + json: {} + - setBody: + simple: '{"id":"${variable.traceId}","content":${body}}' + - setHeader: + name: Content-Type + constant: application/json + otherwise: + steps: + - setHeader: + name: CamelHttpResponseCode + constant: 404 + - setBody: + constant: '{"error":"Trace not found"}' + - setHeader: + name: Content-Type + constant: application/json \ No newline at end of file
