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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/coverage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ code does not implement. Machine-readable: [`coverage.json`](./coverage.json).
| `loadtesting` | - | [LoadTesting](./azure/loadtesting.md) | - | - | 8 |
| `location` | [Location](./aws/location.md) | - | - | - | 28 |
| `locks` | - | [Locks](./azure/locks.md) | - | - | 4 |
| `logging` | [CloudWatchLogs](./aws/cloudwatchlogs.md) | [LogAnalytics](./azure/loganalytics.md) | [CloudLogging](./gcp/cloudlogging.md) | - | 17 |
| `logging` | [CloudWatchLogs](./aws/cloudwatchlogs.md) | [LogAnalytics](./azure/loganalytics.md) | [CloudLogging](./gcp/cloudlogging.md) | [Logging](./oci/logging.md) | 17 |
| `logic` | - | [Logic](./azure/logic.md) | - | - | 14 |
| `lro` | - | - | [LRO](./gcp/lro.md) | - | 1 |
| `managedcassandra` | - | [ManagedCassandra](./azure/managedcassandra.md) | - | - | 15 |
Expand Down
3 changes: 2 additions & 1 deletion docs/coverage/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -10680,7 +10680,8 @@
"providers": {
"aws": "CloudWatchLogs",
"azure": "LogAnalytics",
"gcp": "CloudLogging"
"gcp": "CloudLogging",
"oci": "Logging"
}
},
{
Expand Down
1 change: 1 addition & 0 deletions docs/coverage/oci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Services cloudemu emulates for OCI, by native name. Back to the [cross-provider
| OCI service | Portable service | Operations |
| --- | --- | --- |
| [Identity](./identity.md) | `iam` | 40 |
| [Logging](./logging.md) | `logging` | 17 |
| [Monitoring](./monitoring.md) | `monitoring` | 12 |
| [VCN](./vcn.md) | `networking` | 57 |
| [Workrequest](./workrequest.md) | (provider-native) | 4 |
30 changes: 30 additions & 0 deletions docs/coverage/oci/logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!-- Generated by `go generate ./...` (internal/coveragegen). Do not edit by hand. -->
# Logging

OCI's `logging` service · portable interface `driver.Logging` · [OCI index](./README.md)

## Operations (17)

| Operation | Description |
| --- | --- |
| `CreateLogGroup` | |
| `CreateLogStream` | |
| `DeleteLogGroup` | |
| `DeleteLogStream` | |
| `DeleteMetricFilter` | |
| `DeleteSubscriptionFilter` | |
| `DescribeMetricFilters` | |
| `DescribeSubscriptionFilters` | |
| `FilterLogEvents` | |
| `GetLogEvents` | |
| `GetLogGroup` | |
| `ListLogGroups` | |
| `ListLogStreams` | |
| `PutLogEvents` | |
| `PutMetricFilter` | |
| `PutSubscriptionFilter` | |
| `UpdateLogGroup` | UpdateLogGroup replaces the mutable fields (retention, tags) of an |

## Not in scope

_Not documented yet. See the [emulator boundary](../../../README.md) for cloudemu-wide non-goals._
100 changes: 97 additions & 3 deletions docs/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -1422,7 +1422,7 @@ a source cluster and detach on promote; clone-on-read on every path.
## 13. Logging

**Driver interface:** `services/logging/driver/driver.go`
**AWS:** CloudWatch Logs | **Azure:** Log Analytics | **GCP:** Cloud Logging
**AWS:** CloudWatch Logs | **Azure:** Log Analytics | **GCP:** Cloud Logging | **OCI:** Logging (a log group is the log group; a CUSTOM log is the log stream; an ingested log entry is the log event — metric and subscription filters have no OCI equivalent and report `Unimplemented`)

### Log Group Operations

Expand All @@ -1448,16 +1448,110 @@ a source cluster and detach on promote; clone-on-read on every path.
| `PutLogEvents` | `(ctx, logGroup, streamName, events) error` |
| `GetLogEvents` | `(ctx, input) ([]LogEvent, error)` |

### Filtering and Metric Filters
### Filtering, Metric Filters and Subscription Filters

| Operation | Signature |
|-----------|-----------|
| `FilterLogEvents` | `(ctx, input) ([]FilteredLogEvent, error)` |
| `PutMetricFilter` | `(ctx, config) error` |
| `DeleteMetricFilter` | `(ctx, logGroup, filterName) error` |
| `DescribeMetricFilters` | `(ctx, logGroup) ([]MetricFilterInfo, error)` |
| `PutSubscriptionFilter` | `(ctx, config) error` |
| `DeleteSubscriptionFilter` | `(ctx, logGroup, filterName) error` |
| `DescribeSubscriptionFilters` | `(ctx, logGroup) ([]SubscriptionFilterInfo, error)` |

**Total: 13 operations**
**Total: 17 operations**

### OCI Logging

**Optional capability:** `server/oci/logging.Extras` — OCI addresses log groups
and logs by OCID inside a compartment, gives a log a type and a service source,
batches ingestion, and searches with its own query language, none of which the
portable model carries. Its value types live in `providers/oci/logging`.
**Provider:** `providers/oci/logging` | **Wire:** `server/oci/logging`

OCI publishes the service on three API surfaces, each at its own version
prefix. They collapse onto one CloudEmu server, so `Matches` claims each
prefix's collections exactly. A top-level `/logs` collection belongs to the
ingestion plane alone; the control plane nests logs under their log group.

| Operation | Route |
|-----------|-------|
| `CreateLogGroup` | `POST /20200531/logGroups` |
| `ListLogGroups` | `GET /20200531/logGroups` |
| `GetLogGroup` | `GET /20200531/logGroups/{logGroupId}` |
| `UpdateLogGroup` | `PUT /20200531/logGroups/{logGroupId}` |
| `DeleteLogGroup` | `DELETE /20200531/logGroups/{logGroupId}` |
| `ChangeLogGroupCompartment` | `POST /20200531/logGroups/{logGroupId}/actions/changeCompartment` |
| `CreateLog` | `POST /20200531/logGroups/{logGroupId}/logs` |
| `ListLogs` | `GET /20200531/logGroups/{logGroupId}/logs` |
| `GetLog` | `GET /20200531/logGroups/{logGroupId}/logs/{logId}` |
| `UpdateLog` | `PUT /20200531/logGroups/{logGroupId}/logs/{logId}` |
| `DeleteLog` | `DELETE /20200531/logGroups/{logGroupId}/logs/{logId}` |
| `ChangeLogLogGroup` | `POST /20200531/logGroups/{logGroupId}/logs/{logId}/actions/changeLogGroup` |
| `PutLogs` | `POST /20200831/logs/{logId}/actions/push` |
| `SearchLogs` | `POST /20190909/search` |

The version prefixes are the oci-go-sdk clients' `BasePath`s — `20200531` for
`logging`, `20200831` for `loggingingestion`, `20190909` for `loggingsearch` —
and the handler tests pin them as SDK literals rather than handler constants.

`ListLogGroups` requires `compartmentId`; it, `ListLogs` and `SearchLogs`
paginate with `limit` / `page`, returning the cursor as `opc-next-page`. A
`page` this API never returned is rejected with `400` rather than silently
restarting at the first page. `ListLogs` takes no `compartmentId` —
the log group in the path fixes the compartment, as it does in real OCI — and
narrows on `displayName`, `logType`, `sourceService`, `sourceResource` and
`lifecycleState`. Every log group and log mutation is asynchronous in real OCI,
so each answers `202` with an `opc-work-request-id`; the created resource's
OCID comes back on the work request, whose `operationType` is one of the SDK's
`OperationTypesEnum` values (`CREATE_LOG_GROUP`, `MOVE_LOG_GROUP`, `MOVE_LOG`,
…). Ingestion and search are synchronous.

When the server wires Identity, creating a log group in, or moving one into, a
compartment that does not exist is `404 NotAuthorizedOrNotFound`, as in VCN.
`ChangeLogGroupCompartment` reads the target from `compartmentId`, the
`ChangeLogGroupCompartmentDetails` field. Deleting a log group that still holds
logs is `409 IncorrectState`, as real OCI requires the group empty; the
portable `DeleteLogGroup` is the path that cascades. A log's
`retentionDuration` must be 30 to 180 days in 30-day steps; any other value is
`400`. A log response carries the configured `tenancyId`.

A CUSTOM log takes entries from `PutLogs` and has no service source: it carries
a `configuration` only when the caller supplied one, never a synthesized
`sourceType`. A SERVICE log is fed by the service its `configuration.source`
names, so ingesting into one is refused rather than accepted and dropped, as is
ingesting into a disabled log. Every `LogEntryBatch` must carry `source`, `type`
and `defaultlogentrytime`, which the SDK marks mandatory; a call with a batch
missing one is `400` naming the batch and field, and ingests nothing.

Search queries are read in the form
`search "compartmentId[/logGroupId[/logId]]" | where <field> = '<value>' [and …]
| sort by datetime [asc|desc]`, with `*` as the wildcard and comma-separated
search targets. The tenancy OCID addresses the root compartment, where log
groups created without one land. Everything else is rejected naming what it tripped on rather
than answered with an empty result set: the `summarize`, `stats`, `topN` and
`extract` operators; `or`, `not` and parenthesised where clauses; the `>`, `<`,
`>=`, `<=`, `=~` and `!~` operators; a field the record shape has no place for,
including a nested payload path; a sort on anything but datetime; and a search
target segment written as a name where OCI takes an OCID.

Not emulated: `/20200531/unifiedAgentConfigurations` and
`/20200531/logSavedSearches`, which the logging driver has no shape for. Both
are claimed so a caller gets a `501` naming the gap rather than a bare `404`.
A log group's retention is CloudEmu's: real OCI carries retention on the log,
and the group holds the default its logs inherit so the portable
`RetentionDays` has somewhere to live. Log group display names are unique per
compartment, as in real OCI, so the same name may be used in two compartments.
The portable driver has only a name to address a group by, so a name held in
more than one compartment is rejected as ambiguous rather than resolved
arbitrarily; such a group is reachable through the OCI API by OCID.

A read limit is bounded before it sizes an allocation: `GetLogEvents`,
`FilterLogEvents` and `SearchLogs` reject a negative limit and one above
10000 with `InvalidArgument`. Subscription filters, like metric filters, are
not an OCI Logging operation and report `Unimplemented` — OCI delivers log
entries to another service through a Service Connector.

---

Expand Down
186 changes: 186 additions & 0 deletions providers/oci/logging/group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package logging

import (
"context"

cerrors "github.com/stackshy/cloudemu/v2/errors"
)

// CreateGroup creates a log group. OCI-only: the portable driver has no
// compartment or description.
func (m *Mock) CreateGroup(_ context.Context, spec LogGroupSpec) (*LogGroup, error) {
m.mu.Lock()
defer m.mu.Unlock()

return m.createGroup(spec)
}

// createGroup is CreateGroup with mu already held.
func (m *Mock) createGroup(spec LogGroupSpec) (*LogGroup, error) {
if err := requireName(spec.DisplayName, "log group displayName"); err != nil {
return nil, err
}

compartment := m.compartmentOr(spec.CompartmentID)
if _, ok := m.groupByName(compartment, spec.DisplayName); ok {
return nil, cerrors.Newf(cerrors.AlreadyExists,
"log group %q already exists in compartment %s", spec.DisplayName, compartment)
}

retention := spec.RetentionDays
if retention == 0 {
retention = defaultRetentionDays
}

now := m.now()
g := &LogGroup{
ID: m.newOCID(typeLogGroup),
CompartmentID: compartment,
DisplayName: spec.DisplayName,
Description: spec.Description,
LifecycleState: StateActive,
TimeCreated: now,
TimeLastModified: now,
FreeformTags: copyTags(spec.FreeformTags),
RetentionDays: retention,
}

m.groups.Set(g.ID, g)

out := g.clone()

return &out, nil
}

// GetGroup returns a log group by OCID.
func (m *Mock) GetGroup(_ context.Context, id string) (*LogGroup, error) {
m.mu.RLock()
defer m.mu.RUnlock()

g, ok := m.groups.Get(id)
if !ok {
return nil, cerrors.Newf(cerrors.NotFound, "log group %q not found", id)
}

out := g.clone()

return &out, nil
}

// ListGroups returns the log groups in a compartment, optionally narrowed to
// one display name. Matching is exact: real OCI descends the compartment tree
// only when the caller asks it to.
func (m *Mock) ListGroups(_ context.Context, compartmentID, displayName string) ([]LogGroup, error) {
m.mu.RLock()
defer m.mu.RUnlock()

if err := requireName(compartmentID, compartmentIDName); err != nil {
return nil, err
}

out := make([]LogGroup, 0, m.groups.Len())

for _, g := range m.groups.SortedValues() {
if g.CompartmentID != compartmentID {
continue
}

if displayName != "" && g.DisplayName != displayName {
continue
}

out = append(out, g.clone())
}

return out, nil
}

// UpdateGroup replaces the mutable fields of a log group.
func (m *Mock) UpdateGroup(_ context.Context, id string, u LogGroupUpdate) (*LogGroup, error) {
m.mu.Lock()
defer m.mu.Unlock()

g, ok := m.groups.Get(id)
if !ok {
return nil, cerrors.Newf(cerrors.NotFound, "log group %q not found", id)
}

if u.DisplayName != nil && *u.DisplayName != g.DisplayName {
if _, taken := m.groupByName(g.CompartmentID, *u.DisplayName); taken {
return nil, cerrors.Newf(cerrors.AlreadyExists,
"log group %q already exists in compartment %s", *u.DisplayName, g.CompartmentID)
}

g.DisplayName = *u.DisplayName
}

if u.Description != nil {
g.Description = *u.Description
}

if u.FreeformTags != nil {
g.FreeformTags = copyTags(u.FreeformTags)
}

g.TimeLastModified = m.now()

out := g.clone()

return &out, nil
}

// DeleteGroup deletes an empty log group. Real OCI refuses to delete a group
// that still holds logs, so this does too; the portable DeleteLogGroup is the
// path that cascades.
func (m *Mock) DeleteGroup(_ context.Context, id string) error {
m.mu.Lock()
defer m.mu.Unlock()

if !m.groups.Has(id) {
return cerrors.Newf(cerrors.NotFound, "log group %q not found", id)
}

if n := len(m.logsIn(id)); n > 0 {
return cerrors.Newf(cerrors.FailedPrecondition,
"log group %q still holds %d log(s); delete them before the group", id, n)
}

m.groups.Delete(id)

return nil
}

// MoveGroup moves a log group and its logs to another compartment.
func (m *Mock) MoveGroup(_ context.Context, id, compartmentID string) error {
m.mu.Lock()
defer m.mu.Unlock()

if err := requireName(compartmentID, compartmentIDName); err != nil {
return err
}

g, ok := m.groups.Get(id)
if !ok {
return cerrors.Newf(cerrors.NotFound, "log group %q not found", id)
}

if other, taken := m.groupByName(compartmentID, g.DisplayName); taken && other.ID != id {
return cerrors.Newf(cerrors.AlreadyExists,
"log group %q already exists in compartment %s", g.DisplayName, compartmentID)
}

g.CompartmentID = compartmentID
g.TimeLastModified = m.now()

for _, rec := range m.logsIn(id) {
rec.Log.CompartmentID = compartmentID

if rec.Log.Configuration != nil {
cfg := *rec.Log.Configuration
cfg.CompartmentID = compartmentID
rec.Log.Configuration = &cfg
}
}

return nil
}
Loading
Loading