OCI Logging Analytics: Searching, Correlating, and Alerting on Log Data with Terraform

Logs land in OCI Logging and stay there. Teams search them reactively when something breaks. Nobody knows what normal looks like, so nobody notices when normal starts drifting. Alerts fire based on keyword matches that were configured once and never reviewed. The log data exists but it is not doing any analytical work.

OCI Logging Analytics changes that relationship with log data. It ingests logs from OCI services, on-premises systems, and custom sources, applies parsers to extract structured fields, stores them in a queryable log analytics engine, and lets you run SQL-like queries across hundreds of millions of log records in seconds. It supports saved searches, dashboards, scheduled queries that trigger alarms, machine learning anomaly detection, and correlation across multiple log sources. This post covers setting it up with Terraform, writing useful queries, and setting up intelligent alerting.

Architecture

OCI Services (Audit, VCN Flow, App Logs)
        |
   OCI Logging (collection layer)
        |
   Service Connector Hub
        |
   OCI Logging Analytics (analytical layer)
        |
   Scheduled Queries and Alerts
   Dashboards
   ML Anomaly Detection
        |
   OCI Notifications

Step 1: IAM Policy

resource "oci_identity_policy" "logging_analytics_policy" {
  compartment_id = var.compartment_id
  name           = "logging-analytics-policy"
  description    = "Permissions for OCI Logging Analytics"

  statements = [
    "Allow service loganalytics to read log-groups in compartment id ${var.compartment_id}",
    "Allow service loganalytics to read log-content in compartment id ${var.compartment_id}",
    "Allow group ${var.ops_group_name} to manage loganalytics-features-family in compartment id ${var.compartment_id}",
    "Allow group ${var.ops_group_name} to manage loganalytics-resources-family in compartment id ${var.compartment_id}",
    "Allow group ${var.dev_group_name} to read loganalytics-features-family in compartment id ${var.compartment_id}",
    "Allow service sch to use loganalytics-log-group in compartment id ${var.compartment_id}"
  ]
}

Step 2: Enable Logging Analytics and Create Log Groups

resource "oci_log_analytics_namespace" "production" {
  namespace      = var.tenancy_namespace
  is_onboarded   = true
  compartment_id = var.tenancy_ocid
}

resource "oci_log_analytics_log_analytics_log_group" "infrastructure" {
  compartment_id = var.compartment_id
  namespace      = var.tenancy_namespace
  display_name   = "infrastructure-logs"
  description    = "VCN flow logs, audit logs, load balancer access logs"

  defined_tags = {
    "Operations.Environment" = "production"
    "Operations.ManagedBy"   = "terraform"
  }
}

resource "oci_log_analytics_log_analytics_log_group" "application" {
  compartment_id = var.compartment_id
  namespace      = var.tenancy_namespace
  display_name   = "application-logs"
  description    = "Application service logs"
}

resource "oci_log_analytics_log_analytics_log_group" "database_audit" {
  compartment_id = var.compartment_id
  namespace      = var.tenancy_namespace
  display_name   = "database-audit-logs"
  description    = "Oracle Database audit events"
}

Step 3: Service Connector Hub to Ingest OCI Logs

resource "oci_sch_service_connector" "vcn_flow_to_la" {
  compartment_id = var.compartment_id
  display_name   = "vcn-flow-logs-to-logging-analytics"

  source {
    kind = "logging"
    log_sources {
      compartment_id = var.compartment_id
      log_group_id   = var.vcn_flow_log_group_id
    }
  }

  target {
    kind                  = "loggingAnalytics"
    log_group_id          = oci_log_analytics_log_analytics_log_group.infrastructure.id
    log_source_identifier = "OCI VCN Flow Unified Schema Logs"
  }
}

resource "oci_sch_service_connector" "audit_to_la" {
  compartment_id = var.compartment_id
  display_name   = "audit-logs-to-logging-analytics"

  source {
    kind = "logging"
    log_sources {
      compartment_id = var.compartment_id
      log_group_id   = "_Audit"
    }
  }

  target {
    kind                  = "loggingAnalytics"
    log_group_id          = oci_log_analytics_log_analytics_log_group.infrastructure.id
    log_source_identifier = "OCI Audit Logs"
  }
}

resource "oci_sch_service_connector" "app_logs_to_la" {
  compartment_id = var.compartment_id
  display_name   = "application-logs-to-logging-analytics"

  source {
    kind = "logging"
    log_sources {
      compartment_id = var.compartment_id
      log_group_id   = var.app_log_group_id
    }
  }

  target {
    kind                  = "loggingAnalytics"
    log_group_id          = oci_log_analytics_log_analytics_log_group.application.id
    log_source_identifier = "OCI Logging"
  }
}

The log_source_identifier determines which built-in parser Logging Analytics applies. Using the correct source name for OCI service logs ensures fields are automatically extracted. VCN flow logs parsed with the correct identifier give you structured fields like Source IP, Destination IP, Protocol, Bytes Sent, and Action immediately without any custom parser work.

Step 4: Writing Logging Analytics Queries

Logging Analytics uses Log Query Language (LQL). It resembles SQL with pipeline operators. Each pipe stage transforms or filters the result set from the previous stage.

-- Failed login attempts grouped by source IP, last 24 hours
'Log Source' = 'OCI Audit Logs'
and 'Event Name' = 'Authenticate'
and 'Response Status' != '200'
| timestats count as 'Failed Attempts' by 'IP Address', 'User Name'
| where 'Failed Attempts' > 5
| sort -'Failed Attempts'

-- Top rejected source IPs from VCN flow logs
'Log Source' = 'OCI VCN Flow Unified Schema Logs'
and Action = 'REJECT'
| timestats count as 'Rejected Packets' by 'Source IP'
| sort -'Rejected Packets'
| head 20

-- Application error rate by service over the last 6 hours
'Log Group' = 'application-logs'
| eval level = upper(Level)
| timestats count as 'Total' by level, 'Service Name'
| eval error_rate = round((ERROR / Total) * 100, 2)
| where error_rate > 5
| sort -error_rate

-- IAM policy changes in the last hour
'Log Source' = 'OCI Audit Logs'
and 'Event Name' in ('CreatePolicy', 'UpdatePolicy', 'DeletePolicy',
                      'CreateGroup', 'DeleteGroup', 'AddUserToGroup')
| fields 'Event Time', 'User Name', 'IP Address', 'Event Name', 'Resource Name'
| sort -'Event Time'

Step 5: Saved Searches

resource "oci_log_analytics_log_analytics_saved_search" "failed_logins" {
  compartment_id = var.compartment_id
  namespace      = var.tenancy_namespace
  display_name   = "Failed Login Attempts by IP"
  description    = "Failed OCI Console and API authentication grouped by source IP"
  type           = "SEARCH"

  query_string = "'Log Source' = 'OCI Audit Logs' and 'Event Name' = 'Authenticate' and 'Response Status' != '200' | timestats count as 'Failed Attempts' by 'IP Address', 'User Name' | where 'Failed Attempts' > 3 | sort -'Failed Attempts'"

  defined_tags = {
    "Operations.ManagedBy" = "terraform"
  }
}

resource "oci_log_analytics_log_analytics_saved_search" "iam_changes" {
  compartment_id = var.compartment_id
  namespace      = var.tenancy_namespace
  display_name   = "IAM Policy Changes"
  description    = "All IAM policy create, update, and delete events"
  type           = "SEARCH"

  query_string = "'Log Source' = 'OCI Audit Logs' and 'Event Name' in ('CreatePolicy', 'UpdatePolicy', 'DeletePolicy', 'CreateGroup', 'DeleteGroup', 'AddUserToGroup') | fields 'Event Time', 'User Name', 'IP Address', 'Event Name', 'Resource Name' | sort -'Event Time'"
}

resource "oci_log_analytics_log_analytics_saved_search" "app_error_rate" {
  compartment_id = var.compartment_id
  namespace      = var.tenancy_namespace
  display_name   = "Application Error Rate by Service"
  description    = "Error rate percentage per service"
  type           = "SEARCH"

  query_string = "'Log Group' = 'application-logs' | eval level = upper(Level) | timestats count as 'Total', count where level = 'ERROR' as 'Errors' span = 5minutes by 'Service Name' | eval error_rate = round((Errors / Total) * 100, 2) | sort -error_rate"
}

Step 6: Scheduled Queries and Alerts

resource "oci_log_analytics_scheduled_task" "brute_force_alert" {
  compartment_id = var.compartment_id
  namespace      = var.tenancy_namespace
  display_name   = "Brute Force Detection"
  kind           = "STANDARD"
  task_type      = "SAVED_SEARCH"

  action {
    type            = "STREAM"
    saved_search_id = oci_log_analytics_log_analytics_saved_search.failed_logins.id

    metric_extraction {
      compartment_id = var.compartment_id
      namespace      = "custom_log_analytics"
      metric_name    = "FailedLoginsByIP"
      resource_group = "security"
    }
  }

  schedules {
    schedule {
      type           = "CRON"
      expression     = "0 */5 * * * ?"
      time_zone      = "UTC"
      misfire_policy = "RETRY_ONCE"
    }
  }
}

resource "oci_monitoring_alarm" "brute_force_detected" {
  compartment_id        = var.compartment_id
  display_name          = "brute-force-login-detected"
  is_enabled            = true
  metric_compartment_id = var.compartment_id
  namespace             = "custom_log_analytics"
  query                 = "FailedLoginsByIP[5m]{resourceGroup = 'security'}.max() > 10"
  severity              = "CRITICAL"
  pending_duration      = "PT5M"
  destinations          = [var.security_notification_topic_id]
  body                  = "More than 10 failed login attempts from a single IP in 5 minutes. Review the Failed Login Attempts saved search in Logging Analytics."
}

Step 7: ML Anomaly Detection

The anomalydetect operator uses Seasonal Hybrid ETS to learn baseline behavior from historical log data and flag deviations. It accounts for time-of-day and day-of-week patterns, so a traffic spike at 3am is flagged but a spike at Monday 9am that matches historical Mondays is not.

-- Anomaly detection on VCN egress bytes per destination
'Log Source' = 'OCI VCN Flow Unified Schema Logs'
| timestats sum('Bytes Sent') as 'Total Bytes'
            span = 1hour
            by 'Destination IP'
| anomalydetect 'Total Bytes' using 'Seasonal Hybrid ETS'
| where isAnomaly = true
| sort -'Total Bytes'

-- Anomaly detection on application error rates
'Log Group' = 'application-logs'
and Level = 'ERROR'
| timestats count as 'Error Count'
            span = 5minutes
            by 'Service Name'
| anomalydetect 'Error Count'
| where isAnomaly = true

-- Anomaly detection on login volumes
'Log Source' = 'OCI Audit Logs'
and 'Event Name' = 'Authenticate'
| timestats count as 'Login Count' span = 15minutes
| anomalydetect 'Login Count'
| where isAnomaly = true

Step 8: Cross-Source Correlation

The most powerful use of Logging Analytics is correlating events across multiple log sources. A failed API call in the application log, a rejected VCN flow entry from the same source IP, and an IAM authentication event for the same user within the same 5-minute window tells a completely different story than any of those events viewed in isolation.

-- Find source IPs generating both application 403 errors and VCN rejected traffic
-- within the same 5-minute window (potential scanning activity)

-- Step 1: Get application 403 events
'Log Group' = 'application-logs'
and Status = '403'
| eval time_bucket = bin('Event Time', 5minutes)
| stats count as '403_count' by 'Source IP', time_bucket
| where '403_count' > 5

-- Step 2: Correlate with VCN rejections from same IPs
-- Run in linked view or use the link operator
'Log Source' = 'OCI VCN Flow Unified Schema Logs'
and Action = 'REJECT'
| eval time_bucket = bin('Event Time', 5minutes)
| stats count as 'Rejected_Packets' by 'Source IP', time_bucket
| link inner on 'Source IP' within 5minutes
         with 'Log Group' = 'application-logs' and Status = '403'
| where '403_count' > 5 and 'Rejected_Packets' > 10
| fields 'Source IP', time_bucket, '403_count', 'Rejected_Packets'
| sort -'403_count'

Operational Notes

Log volume drives Logging Analytics cost. Before routing all logs into Logging Analytics, decide which sources genuinely benefit from long-term analytical storage. OCI Audit logs, VCN flow logs, and application error logs are high-value candidates. Debug logs at every level from every service are expensive and rarely queried analytically. Set application log levels to INFO or WARN for the streams feeding Logging Analytics.

Build your saved searches iteratively starting from the questions your team asks most often during incidents: what changed in IAM in the last hour, which services have elevated error rates, which source IPs are generating rejected VCN traffic. Build saved searches for those first. Use them during the next incident. Add the next layer based on what you wished you had seen faster.

Anomaly detection requires at least two weeks of historical data before its baseline is reliable. Enable it early, accept that the first two weeks will produce false positives, and tune the sensitivity threshold after the model has seen enough normal behavior to distinguish it from genuine anomalies.

Regards,
Osama

#OCI #OracleCloud #LoggingAnalytics #Observability #Terraform #IaC #OracleCloudInfrastructure #PlatformEngineering #CloudArchitecture #DevOps #TechBlog #Oracle #LogManagement #SIEM #CloudSecurity #AnomalyDetection #LogAnalysis #CloudMonitoring #ServiceConnectorHub #IncidentResponse

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.