Log more information to the action logs and display the namespaces for superusers
This helps superusers understand better what, exactly, is going on in the registry
This commit is contained in:
parent
2b189694a8
commit
11c931f781
10 changed files with 88 additions and 40 deletions
|
@ -62,13 +62,22 @@ def get_aggregated_logs(start_time, end_time, performer=None, repository=None, n
|
||||||
def get_logs_query(start_time, end_time, performer=None, repository=None, namespace=None,
|
def get_logs_query(start_time, end_time, performer=None, repository=None, namespace=None,
|
||||||
ignore=None):
|
ignore=None):
|
||||||
Performer = User.alias()
|
Performer = User.alias()
|
||||||
|
Account = User.alias()
|
||||||
selections = [LogEntry, Performer]
|
selections = [LogEntry, Performer]
|
||||||
|
|
||||||
|
if namespace is None and repository is None:
|
||||||
|
selections.append(Account)
|
||||||
|
|
||||||
query = _logs_query(selections, start_time, end_time, performer, repository, namespace, ignore)
|
query = _logs_query(selections, start_time, end_time, performer, repository, namespace, ignore)
|
||||||
query = (query.switch(LogEntry)
|
query = (query.switch(LogEntry)
|
||||||
.join(Performer, JOIN_LEFT_OUTER,
|
.join(Performer, JOIN_LEFT_OUTER,
|
||||||
on=(LogEntry.performer == Performer.id).alias('performer')))
|
on=(LogEntry.performer == Performer.id).alias('performer')))
|
||||||
|
|
||||||
|
if namespace is None and repository is None:
|
||||||
|
query = (query.switch(LogEntry)
|
||||||
|
.join(Account, JOIN_LEFT_OUTER,
|
||||||
|
on=(LogEntry.account == Account.id).alias('account')))
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ LOGS_PER_PAGE = 20
|
||||||
SERVICE_LEVEL_LOG_KINDS = set(['service_key_create', 'service_key_approve', 'service_key_delete',
|
SERVICE_LEVEL_LOG_KINDS = set(['service_key_create', 'service_key_approve', 'service_key_delete',
|
||||||
'service_key_modify', 'service_key_extend', 'service_key_rotate'])
|
'service_key_modify', 'service_key_extend', 'service_key_rotate'])
|
||||||
|
|
||||||
def log_view(log, kinds):
|
def log_view(log, kinds, include_namespace):
|
||||||
view = {
|
view = {
|
||||||
'kind': kinds[log.kind_id],
|
'kind': kinds[log.kind_id],
|
||||||
'metadata': json.loads(log.metadata_json),
|
'metadata': json.loads(log.metadata_json),
|
||||||
|
@ -33,9 +33,24 @@ def log_view(log, kinds):
|
||||||
'kind': 'user',
|
'kind': 'user',
|
||||||
'name': log.performer.username,
|
'name': log.performer.username,
|
||||||
'is_robot': log.performer.robot,
|
'is_robot': log.performer.robot,
|
||||||
'avatar': avatar.get_data_for_user(log.performer)
|
'avatar': avatar.get_data_for_user(log.performer),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if include_namespace:
|
||||||
|
if log.account and log.account.username:
|
||||||
|
if log.account.organization:
|
||||||
|
view['namespace'] = {
|
||||||
|
'kind': 'org',
|
||||||
|
'name': log.account.username,
|
||||||
|
'avatar': avatar.get_data_for_org(log.account),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
view['namespace'] = {
|
||||||
|
'kind': 'user',
|
||||||
|
'name': log.account.username,
|
||||||
|
'avatar': avatar.get_data_for_user(log.account),
|
||||||
|
}
|
||||||
|
|
||||||
return view
|
return view
|
||||||
|
|
||||||
def aggregated_log_view(log, kinds, start_time):
|
def aggregated_log_view(log, kinds, start_time):
|
||||||
|
@ -92,10 +107,11 @@ def get_logs(start_time, end_time, performer_name=None, repository=None, namespa
|
||||||
logs, next_page_token = model.modelutil.paginate(logs_query, database.LogEntry, descending=True,
|
logs, next_page_token = model.modelutil.paginate(logs_query, database.LogEntry, descending=True,
|
||||||
page_token=page_token, limit=LOGS_PER_PAGE)
|
page_token=page_token, limit=LOGS_PER_PAGE)
|
||||||
|
|
||||||
|
include_namespace = namespace is None and repository is None
|
||||||
return {
|
return {
|
||||||
'start_time': format_date(start_time),
|
'start_time': format_date(start_time),
|
||||||
'end_time': format_date(end_time),
|
'end_time': format_date(end_time),
|
||||||
'logs': [log_view(log, kinds) for log in logs],
|
'logs': [log_view(log, kinds, include_namespace) for log in logs],
|
||||||
}, next_page_token
|
}, next_page_token
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,8 @@ class RepositoryManifestLabels(RepositoryParamResource):
|
||||||
'value': label_data['value'],
|
'value': label_data['value'],
|
||||||
'manifest_digest': manifestref,
|
'manifest_digest': manifestref,
|
||||||
'media_type': label_data['media_type'],
|
'media_type': label_data['media_type'],
|
||||||
|
'namespace': namespace,
|
||||||
|
'repo': repository,
|
||||||
}
|
}
|
||||||
|
|
||||||
log_action('manifest_label_add', namespace, metadata, repo=tag_manifest.tag.repository)
|
log_action('manifest_label_add', namespace, metadata, repo=tag_manifest.tag.repository)
|
||||||
|
@ -150,7 +152,9 @@ class ManageRepositoryManifestLabel(RepositoryParamResource):
|
||||||
'id': labelid,
|
'id': labelid,
|
||||||
'key': deleted.key,
|
'key': deleted.key,
|
||||||
'value': deleted.value,
|
'value': deleted.value,
|
||||||
'manifest_digest': manifestref
|
'manifest_digest': manifestref,
|
||||||
|
'namespace': namespace,
|
||||||
|
'repo': repository,
|
||||||
}
|
}
|
||||||
|
|
||||||
log_action('manifest_label_delete', namespace, metadata, repo=tag_manifest.tag.repository)
|
log_action('manifest_label_delete', namespace, metadata, repo=tag_manifest.tag.repository)
|
||||||
|
|
|
@ -194,6 +194,7 @@ class RepositoryUserPermission(RepositoryParamResource):
|
||||||
|
|
||||||
log_action('change_repo_permission', namespace,
|
log_action('change_repo_permission', namespace,
|
||||||
{'username': username, 'repo': repository,
|
{'username': username, 'repo': repository,
|
||||||
|
'namespace': namespace,
|
||||||
'role': new_permission['role']},
|
'role': new_permission['role']},
|
||||||
repo=model.repository.get_repository(namespace, repository))
|
repo=model.repository.get_repository(namespace, repository))
|
||||||
|
|
||||||
|
@ -209,7 +210,7 @@ class RepositoryUserPermission(RepositoryParamResource):
|
||||||
raise request_error(exception=ex)
|
raise request_error(exception=ex)
|
||||||
|
|
||||||
log_action('delete_repo_permission', namespace,
|
log_action('delete_repo_permission', namespace,
|
||||||
{'username': username, 'repo': repository},
|
{'username': username, 'repo': repository, 'namespace': namespace},
|
||||||
repo=model.repository.get_repository(namespace, repository))
|
repo=model.repository.get_repository(namespace, repository))
|
||||||
|
|
||||||
return '', 204
|
return '', 204
|
||||||
|
|
|
@ -340,7 +340,7 @@ class Repository(RepositoryParamResource):
|
||||||
repo.save()
|
repo.save()
|
||||||
|
|
||||||
log_action('set_repo_description', namespace,
|
log_action('set_repo_description', namespace,
|
||||||
{'repo': repository, 'description': values['description']},
|
{'repo': repository, 'namespace': namespace, 'description': values['description']},
|
||||||
repo=repo)
|
repo=repo)
|
||||||
return {
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
|
@ -404,6 +404,6 @@ class RepositoryVisibility(RepositoryParamResource):
|
||||||
|
|
||||||
model.repository.set_repository_visibility(repo, visibility)
|
model.repository.set_repository_visibility(repo, visibility)
|
||||||
log_action('change_repo_visibility', namespace,
|
log_action('change_repo_visibility', namespace,
|
||||||
{'repo': repository, 'visibility': values['visibility']},
|
{'repo': repository, 'namespace': namespace, 'visibility': values['visibility']},
|
||||||
repo=repo)
|
repo=repo)
|
||||||
return {'success': True}
|
return {'success': True}
|
||||||
|
|
|
@ -102,7 +102,8 @@ class RepositoryNotificationList(RepositoryParamResource):
|
||||||
|
|
||||||
resp = notification_view(new_notification)
|
resp = notification_view(new_notification)
|
||||||
log_action('add_repo_notification', namespace,
|
log_action('add_repo_notification', namespace,
|
||||||
{'repo': repository, 'notification_id': new_notification.uuid,
|
{'repo': repository, 'namespace': namespace,
|
||||||
|
'notification_id': new_notification.uuid,
|
||||||
'event': parsed['event'], 'method': parsed['method']},
|
'event': parsed['event'], 'method': parsed['method']},
|
||||||
repo=repo)
|
repo=repo)
|
||||||
return resp, 201
|
return resp, 201
|
||||||
|
@ -143,7 +144,7 @@ class RepositoryNotification(RepositoryParamResource):
|
||||||
""" Deletes the specified notification. """
|
""" Deletes the specified notification. """
|
||||||
deleted = model.notification.delete_repo_notification(namespace, repository, uuid)
|
deleted = model.notification.delete_repo_notification(namespace, repository, uuid)
|
||||||
log_action('delete_repo_notification', namespace,
|
log_action('delete_repo_notification', namespace,
|
||||||
{'repo': repository, 'notification_id': uuid,
|
{'repo': repository, 'namespace': namespace, 'notification_id': uuid,
|
||||||
'event': deleted.event.name, 'method': deleted.method.name},
|
'event': deleted.event.name, 'method': deleted.method.name},
|
||||||
repo=model.repository.get_repository(namespace, repository))
|
repo=model.repository.get_repository(namespace, repository))
|
||||||
|
|
||||||
|
|
|
@ -106,7 +106,8 @@ class RepositoryTag(RepositoryParamResource):
|
||||||
username = get_authenticated_user().username
|
username = get_authenticated_user().username
|
||||||
log_action('move_tag' if original_image_id else 'create_tag', namespace,
|
log_action('move_tag' if original_image_id else 'create_tag', namespace,
|
||||||
{'username': username, 'repo': repository, 'tag': tag,
|
{'username': username, 'repo': repository, 'tag': tag,
|
||||||
'image': image_id, 'original_image': original_image_id},
|
'namespace': namespace, 'image': image_id,
|
||||||
|
'original_image': original_image_id},
|
||||||
repo=model.repository.get_repository(namespace, repository))
|
repo=model.repository.get_repository(namespace, repository))
|
||||||
|
|
||||||
return 'Updated', 201
|
return 'Updated', 201
|
||||||
|
@ -119,7 +120,7 @@ class RepositoryTag(RepositoryParamResource):
|
||||||
|
|
||||||
username = get_authenticated_user().username
|
username = get_authenticated_user().username
|
||||||
log_action('delete_tag', namespace,
|
log_action('delete_tag', namespace,
|
||||||
{'username': username, 'repo': repository, 'tag': tag},
|
{'username': username, 'repo': repository, 'namespace': namespace, 'tag': tag},
|
||||||
repo=model.repository.get_repository(namespace, repository))
|
repo=model.repository.get_repository(namespace, repository))
|
||||||
|
|
||||||
return '', 204
|
return '', 204
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
<span class="avatar" size="avatarSize || 16" data="entity.avatar"></span>
|
<span class="avatar" size="avatarSize || 16" data="entity.avatar"></span>
|
||||||
<span class="entity-name anchor" href="/organization/{{ entity.name }}"
|
<span class="entity-name anchor" href="/organization/{{ entity.name }}"
|
||||||
is-only-text="!getIsAdmin(entity.name)">
|
is-only-text="!getIsAdmin(entity.name)">
|
||||||
|
{{ entity.name }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
|
@ -44,14 +44,20 @@
|
||||||
infinite-scroll-distance="2">
|
infinite-scroll-distance="2">
|
||||||
<table class="cor-table">
|
<table class="cor-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
<td ng-if="allLogs == 'true'">Namespace</td>
|
||||||
<td>Description</td>
|
<td>Description</td>
|
||||||
<td style="min-width: 226px">Date/Time</td>
|
<td style="min-width: 226px">Date/Time</td>
|
||||||
<td>User/Token/App</td>
|
<td>Performing User/Token/App</td>
|
||||||
<td>IP Address</td>
|
<td>IP Address</td>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tr class="log" ng-repeat="log in (logs | visibleLogFilter:kindsAllowed | filter:search)"
|
<tr class="log" ng-repeat="log in (logs | visibleLogFilter:kindsAllowed | filter:search)"
|
||||||
bindonce>
|
bindonce>
|
||||||
|
<td ng-if="allLogs == 'true'">
|
||||||
|
<span ng-if="log.namespace">
|
||||||
|
<span class="entity-reference" entity="log.namespace" namespace="log.namespace.name"></span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="circle" style="{{ 'background: ' + getColor(log.kind, chart) }}"></span>
|
<span class="circle" style="{{ 'background: ' + getColor(log.kind, chart) }}"></span>
|
||||||
<span class="log-description" bo-html="getDescription(log)"></span>
|
<span class="log-description" bo-html="getDescription(log)"></span>
|
||||||
|
|
|
@ -55,20 +55,20 @@ angular.module('quay').directive('logsView', function () {
|
||||||
'account_change_cc': 'Update credit card',
|
'account_change_cc': 'Update credit card',
|
||||||
'account_change_password': 'Change password',
|
'account_change_password': 'Change password',
|
||||||
'account_convert': 'Convert account to organization',
|
'account_convert': 'Convert account to organization',
|
||||||
'create_robot': 'Create Robot Account: {robot}',
|
'create_robot': 'Create Robot Account {robot}',
|
||||||
'delete_robot': 'Delete Robot Account: {robot}',
|
'delete_robot': 'Delete Robot Account {robot}',
|
||||||
'create_repo': 'Create Repository: {repo}',
|
'create_repo': 'Create Repository {namespace}/{repo}',
|
||||||
'push_repo': function(metadata) {
|
'push_repo': function(metadata) {
|
||||||
if (metadata.tag) {
|
if (metadata.tag) {
|
||||||
return 'Push of {tag}';
|
return 'Push of {tag} to repository {namespace}/{repo}';
|
||||||
} else {
|
} else {
|
||||||
return 'Repository push';
|
return 'Repository push to {namespace}/{repo}';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'repo_verb': function(metadata) {
|
'repo_verb': function(metadata) {
|
||||||
var prefix = '';
|
var prefix = '';
|
||||||
if (metadata.verb == 'squash') {
|
if (metadata.verb == 'squash') {
|
||||||
prefix = 'Pull of squashed tag {tag}'
|
prefix = 'Pull of squashed tag {tag} from {namespace}/{repo}'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metadata.token) {
|
if (metadata.token) {
|
||||||
|
@ -86,11 +86,11 @@ angular.module('quay').directive('logsView', function () {
|
||||||
return prefix;
|
return prefix;
|
||||||
},
|
},
|
||||||
'pull_repo': function(metadata) {
|
'pull_repo': function(metadata) {
|
||||||
var description = 'repository';
|
var description = 'repository {namespace}/{repo}';
|
||||||
if (metadata.tag) {
|
if (metadata.tag) {
|
||||||
description = 'tag {tag}';
|
description = 'tag {tag} from repository {namespace}/{repo}';
|
||||||
} else if (metadata.manifest_digest) {
|
} else if (metadata.manifest_digest) {
|
||||||
description = 'digest {manifest_digest}';
|
description = 'digest {manifest_digest} from repository {namespace}/{repo}';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metadata.token) {
|
if (metadata.token) {
|
||||||
|
@ -109,40 +109,40 @@ angular.module('quay').directive('logsView', function () {
|
||||||
'delete_repo': 'Delete repository: {repo}',
|
'delete_repo': 'Delete repository: {repo}',
|
||||||
'change_repo_permission': function(metadata) {
|
'change_repo_permission': function(metadata) {
|
||||||
if (metadata.username) {
|
if (metadata.username) {
|
||||||
return 'Change permission for user {username} in repository {repo} to {role}';
|
return 'Change permission for user {username} in repository {namespace}/{repo} to {role}';
|
||||||
} else if (metadata.team) {
|
} else if (metadata.team) {
|
||||||
return 'Change permission for team {team} in repository {repo} to {role}';
|
return 'Change permission for team {team} in repository {namespace}/{repo} to {role}';
|
||||||
} else if (metadata.token) {
|
} else if (metadata.token) {
|
||||||
return 'Change permission for token {token} in repository {repo} to {role}';
|
return 'Change permission for token {token} in repository {namespace}/{repo} to {role}';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'delete_repo_permission': function(metadata) {
|
'delete_repo_permission': function(metadata) {
|
||||||
if (metadata.username) {
|
if (metadata.username) {
|
||||||
return 'Remove permission for user {username} from repository {repo}';
|
return 'Remove permission for user {username} from repository {namespace}/{repo}';
|
||||||
} else if (metadata.team) {
|
} else if (metadata.team) {
|
||||||
return 'Remove permission for team {team} from repository {repo}';
|
return 'Remove permission for team {team} from repository {namespace}/{repo}';
|
||||||
} else if (metadata.token) {
|
} else if (metadata.token) {
|
||||||
return 'Remove permission for token {token} from repository {repo}';
|
return 'Remove permission for token {token} from repository {namespace}/{repo}';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'revert_tag': 'Tag {tag} reverted to image {image} from image {original_image}',
|
'revert_tag': 'Tag {tag} reverted to image {image} from image {original_image}',
|
||||||
'delete_tag': 'Tag {tag} deleted in repository {repo} by user {username}',
|
'delete_tag': 'Tag {tag} deleted in repository {namespace}/{repo} by user {username}',
|
||||||
'create_tag': 'Tag {tag} created in repository {repo} on image {image} by user {username}',
|
'create_tag': 'Tag {tag} created in repository {namespace}/{repo} on image {image} by user {username}',
|
||||||
'move_tag': 'Tag {tag} moved from image {original_image} to image {image} in repository {repo} by user {username}',
|
'move_tag': 'Tag {tag} moved from image {original_image} to image {image} in repository {namespace}/{repo} by user {username}',
|
||||||
'change_repo_visibility': 'Change visibility for repository {repo} to {visibility}',
|
'change_repo_visibility': 'Change visibility for repository {namespace}/{repo} to {visibility}',
|
||||||
'add_repo_accesstoken': 'Create access token {token} in repository {repo}',
|
'add_repo_accesstoken': 'Create access token {token} in repository {repo}',
|
||||||
'delete_repo_accesstoken': 'Delete access token {token} in repository {repo}',
|
'delete_repo_accesstoken': 'Delete access token {token} in repository {repo}',
|
||||||
'set_repo_description': 'Change description for repository {repo}',
|
'set_repo_description': 'Change description for repository {namespace}/{repo}',
|
||||||
'build_dockerfile': function(metadata) {
|
'build_dockerfile': function(metadata) {
|
||||||
if (metadata.trigger_id) {
|
if (metadata.trigger_id) {
|
||||||
var triggerDescription = TriggerService.getDescription(
|
var triggerDescription = TriggerService.getDescription(
|
||||||
metadata['service'], metadata['config']);
|
metadata['service'], metadata['config']);
|
||||||
return 'Build image from Dockerfile for repository {repo} triggered by ' + triggerDescription;
|
return 'Build image from Dockerfile for repository {namespace}/{repo} triggered by ' + triggerDescription;
|
||||||
}
|
}
|
||||||
return 'Build image from Dockerfile for repository {repo}';
|
return 'Build image from Dockerfile for repository {namespace}/{repo}';
|
||||||
},
|
},
|
||||||
'org_create_team': 'Create team: {team}',
|
'org_create_team': 'Create team {team}',
|
||||||
'org_delete_team': 'Delete team: {team}',
|
'org_delete_team': 'Delete team {team}',
|
||||||
'org_add_team_member': 'Add member {member} to team {team}',
|
'org_add_team_member': 'Add member {member} to team {team}',
|
||||||
'org_remove_team_member': 'Remove member {member} from team {team}',
|
'org_remove_team_member': 'Remove member {member} from team {team}',
|
||||||
'org_invite_team_member': function(metadata) {
|
'org_invite_team_member': function(metadata) {
|
||||||
|
@ -204,12 +204,12 @@ angular.module('quay').directive('logsView', function () {
|
||||||
|
|
||||||
'add_repo_notification': function(metadata) {
|
'add_repo_notification': function(metadata) {
|
||||||
var eventData = ExternalNotificationData.getEventInfo(metadata.event);
|
var eventData = ExternalNotificationData.getEventInfo(metadata.event);
|
||||||
return 'Add notification of event "' + eventData['title'] + '" for repository {repo}';
|
return 'Add notification of event "' + eventData['title'] + '" for repository {namespace}/{repo}';
|
||||||
},
|
},
|
||||||
|
|
||||||
'delete_repo_notification': function(metadata) {
|
'delete_repo_notification': function(metadata) {
|
||||||
var eventData = ExternalNotificationData.getEventInfo(metadata.event);
|
var eventData = ExternalNotificationData.getEventInfo(metadata.event);
|
||||||
return 'Delete notification of event "' + eventData['title'] + '" for repository {repo}';
|
return 'Delete notification of event "' + eventData['title'] + '" for repository {namespace}/{repo}';
|
||||||
},
|
},
|
||||||
|
|
||||||
'regenerate_robot_token': 'Regenerated token for robot {robot}',
|
'regenerate_robot_token': 'Regenerated token for robot {robot}',
|
||||||
|
@ -236,8 +236,8 @@ angular.module('quay').directive('logsView', function () {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'manifest_label_add': 'Label {key} added to manifest {manifest_digest}',
|
'manifest_label_add': 'Label {key} added to manifest {manifest_digest} under repository {namespace}/{repo}',
|
||||||
'manifest_label_delete': 'Label {key} deleted from manifest {manifest_digest}',
|
'manifest_label_delete': 'Label {key} deleted from manifest {manifest_digest} under repository {namespace}/{repo}',
|
||||||
|
|
||||||
// Note: These are deprecated.
|
// Note: These are deprecated.
|
||||||
'add_repo_webhook': 'Add webhook in repository {repo}',
|
'add_repo_webhook': 'Add webhook in repository {repo}',
|
||||||
|
@ -400,6 +400,15 @@ angular.module('quay').directive('logsView', function () {
|
||||||
|
|
||||||
$scope.getDescription = function(log) {
|
$scope.getDescription = function(log) {
|
||||||
log.metadata['_ip'] = log.ip ? log.ip : null;
|
log.metadata['_ip'] = log.ip ? log.ip : null;
|
||||||
|
|
||||||
|
// Note: This is for back-compat for logs that previously did not have namespaces.
|
||||||
|
// TODO(jschorr): Remove this after a month or two (~April 2017).
|
||||||
|
var namespace = '';
|
||||||
|
if (log.namespace) {
|
||||||
|
namespace = log.namespace.username || log.namespace.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.metadata['namespace'] = log.metadata['namespace'] || namespace || '';
|
||||||
return StringBuilderService.buildString(logDescriptions[log.kind] || log.kind, log.metadata);
|
return StringBuilderService.buildString(logDescriptions[log.kind] || log.kind, log.metadata);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Reference in a new issue