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 @@ -60,7 +60,7 @@ code does not implement. Machine-readable: [`coverage.json`](./coverage.json).
| `kusto` | — | [Kusto](./azure/kusto.md) | — | — | 14 |
| `loadbalancer` | [ELB](./aws/elb.md) | [LB](./azure/lb.md) | [LB](./gcp/lb.md) | — | 19 |
| `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 |
| `lro` | — | — | [LRO](./gcp/lro.md) | — | 1 |
| `managedcassandra` | — | [ManagedCassandra](./azure/managedcassandra.md) | — | — | 15 |
| `managedidentity` | — | [ManagedIdentity](./azure/managedidentity.md) | — | — | 8 |
Expand Down
3 changes: 2 additions & 1 deletion docs/coverage/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -7128,7 +7128,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._
77 changes: 74 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,87 @@ 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}` |
| `PutLogs` | `POST /20200601/logs/{logId}/actions/push` |
| `SearchLogs` | `POST /20190909/search` |

`ListLogGroups` requires `compartmentId` and paginates with `limit` / `page`,
returning the cursor as `opc-next-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. Ingestion and search are synchronous.

A CUSTOM log takes entries from `PutLogs`; 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.

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. 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
182 changes: 182 additions & 0 deletions providers/oci/logging/group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
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

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

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, "compartmentId"); 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)
}

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

return &out, nil
}

// DeleteGroup deletes a log group and the logs inside it. Deleting the group
// discards their entries with them.
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)
}

for _, rec := range m.logsIn(id) {
m.logs.Delete(rec.Log.ID)
}

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, "compartmentId"); 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 {
rec.Log.Configuration.CompartmentID = compartmentID
}
}

return nil
}
Loading
Loading